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

Compare commits

...

3 Commits

Author SHA1 Message Date
29775e2e75
make goreleaser include modules subdir 2020-10-22 18:13:11 +01:00
9d62b70daa
Fix esbuild and snapshot versioning in goreleaser
snapshots are now "jfa-go_git-<commit>..." instead of
"jfa-go_v0.0.0...".
2020-10-22 17:57:05 +01:00
301f502052
Rework typescript to use modules
web UI now uses modules, and relies less on bodge to make things work.
Also fixes an issue where invites where "failed to send to xx" appeared
in invite form.
2020-10-22 17:50:40 +01:00
32 changed files with 1002 additions and 853 deletions

1
.gitignore vendored
View File

@ -8,6 +8,7 @@ data/static/*.css
data/static/*.js data/static/*.js
data/static/*.js.map data/static/*.js.map
data/static/ts/ data/static/ts/
data/static/modules/
!data/static/setup.js !data/static/setup.js
data/config-base.json data/config-base.json
data/config-default.ini data/config-default.ini

View File

@ -16,7 +16,7 @@ before:
- python3 scss/compile.py - python3 scss/compile.py
- python3 mail/generate.py - python3 mail/generate.py
- python3 version.py {{.Version}} version.go - python3 version.py {{.Version}} version.go
- bash -c 'npx esbuild ts/* --outdir=data/static --minify' - bash -c 'npx esbuild ts/*.ts ts/modules/*.ts --outdir=data/static --minify'
- go get -u github.com/swaggo/swag/cmd/swag - go get -u github.com/swaggo/swag/cmd/swag
- swag init -g main.go - swag init -g main.go
builds: builds:
@ -41,10 +41,11 @@ archives:
- data/* - data/*
- data/templates/* - data/templates/*
- data/static/* - data/static/*
- data/static/modules/*
checksum: checksum:
name_template: 'checksums.txt' name_template: 'checksums.txt'
snapshot: snapshot:
name_template: "{{ .Tag }}-testing" name_template: "git-{{.ShortCommit}}"
changelog: changelog:
sort: asc sort: asc
filters: filters:

View File

@ -18,12 +18,15 @@ email:
typescript: typescript:
$(info Compiling typescript) $(info Compiling typescript)
npx esbuild ts/* --outdir=data/static --minify npx esbuild ts/*.ts ts/modules/*.ts --outdir=data/static --minify
-rm -r data/static/ts -rm -r data/static/ts
-rm -r data/static/typings
-rm data/static/*.map -rm data/static/*.map
ts-debug: ts-debug:
-npx tsc -p ts/ --sourceMap -npx tsc -p ts/ --sourceMap
-rm -r data/static/ts
-rm -r data/static/typings
cp -r ts data/static/ cp -r ts data/static/
swagger: swagger:
@ -51,3 +54,4 @@ install:
cp -r build $(DESTDIR)/jfa-go cp -r build $(DESTDIR)/jfa-go
all: configuration sass email version typescript swagger compile copy all: configuration sass email version typescript swagger compile copy
debug: configuration sass email version ts-debug swagger compile copy

4
api.go
View File

@ -626,12 +626,12 @@ func (app *appContext) DeleteProfile(gc *gin.Context) {
// @tags Invites // @tags Invites
func (app *appContext) GetInvites(gc *gin.Context) { func (app *appContext) GetInvites(gc *gin.Context) {
app.debug.Println("Invites requested") app.debug.Println("Invites requested")
current_time := time.Now() currentTime := time.Now()
app.storage.loadInvites() app.storage.loadInvites()
app.checkInvites() app.checkInvites()
var invites []inviteDTO var invites []inviteDTO
for code, inv := range app.storage.invites { for code, inv := range app.storage.invites {
_, _, days, hours, minutes, _ := timeDiff(inv.ValidTill, current_time) _, _, days, hours, minutes, _ := timeDiff(inv.ValidTill, currentTime)
invite := inviteDTO{ invite := inviteDTO{
Code: code, Code: code,
Days: days, Days: days,

View File

@ -31,11 +31,11 @@
return ""; return "";
} }
{{ if .bs5 }} {{ if .bs5 }}
var bsVersion = 5; window.bsVersion = 5;
{{ else }} {{ else }}
var bsVersion = 4; window.bsVersion = 4;
{{ end }} {{ end }}
var cssFile = "{{ .cssFile }}"; window.cssFile = "{{ .cssFile }}";
var css = document.createElement('link'); var css = document.createElement('link');
css.setAttribute('rel', 'stylesheet'); css.setAttribute('rel', 'stylesheet');
css.setAttribute('type', 'text/css'); css.setAttribute('type', 'text/css');
@ -465,27 +465,19 @@
<p>{{ .contactMessage }}</p> <p>{{ .contactMessage }}</p>
</div> </div>
</div> </div>
<script src="common.js"></script>
<script> <script>
var availableProfiles = []; window.bs5 = {{ .bs5 }};
window.availableProfiles = [];
{{ if .notifications }} {{ if .notifications }}
var notifications_enabled = true; window.notifications_enabled = true;
{{ else }} {{ else }}
var notifications_enabled = false; window.notifications_enabled = false;
{{ end }} {{ end }}
</script> </script>
{{ if .bs5 }} <script src="admin.js" type="module"></script>
<script src="bs5.js"></script> <script src="invites.js" type="module"></script>
{{ else }}
<script src="bs4.js"></script>
{{ end }}
<script src="animation.js"></script>
<script src="accounts.js"></script>
<script src="invites.js"></script>
<script src="admin.js"></script>
<script src="settings.js"></script>
{{ if .ombiEnabled }} {{ if .ombiEnabled }}
<script src="ombi.js"></script> <script src="ombi.js" type="module"></script>
{{ end }} {{ end }}
</body> </body>
</html> </html>

View File

@ -0,0 +1,7 @@
{{ define "form-base" }}
<script>
window.bs5 = {{ .bs5 }};
window.usernameEnabled = {{ .username }};
</script>
<script src="form.js" type="module"></script>
{{ end }}

View File

@ -0,0 +1 @@
{{ template "form.html" . }}

View File

@ -14,11 +14,11 @@
<!-- Bootstrap CSS --> <!-- Bootstrap CSS -->
<link rel="stylesheet" type="text/css" href="{{ .cssFile }}"> <link rel="stylesheet" type="text/css" href="{{ .cssFile }}">
{{ if not .bs5 }} {{ if not .settings.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>
{{ end }} {{ end }}
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
{{ if .bs5 }} {{ if .settings.bs5 }}
<script src="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/js/bootstrap.min.js" integrity="sha384-oesi62hOLfzrys4LxRF63OJCXdXDipiYWBnvTl9Y9/TRlw5xlKIEHpNyvvDShgf/" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/js/bootstrap.min.js" integrity="sha384-oesi62hOLfzrys4LxRF63OJCXdXDipiYWBnvTl9Y9/TRlw5xlKIEHpNyvvDShgf/" crossorigin="anonymous"></script>
{{ else }} {{ else }}
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
@ -74,9 +74,9 @@
<form action="#" method="POST" id="accountForm"> <form action="#" method="POST" id="accountForm">
<div class="form-group"> <div class="form-group">
<label for="inputEmail">Email</label> <label for="inputEmail">Email</label>
<input type="email" class="form-control" id="{{ if .username }}inputEmail{{ else }}inputUsername{{ end }}" name="{{ if .username }}email{{ else }}username{{ end }}" placeholder="Email" 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 .username }} {{ if .settings.username }}
<div class="form-group"> <div class="form-group">
<label for="inputUsername">Username</label> <label for="inputUsername">Username</label>
<input type="username" class="form-control" id="inputUsername" name="username" placeholder="Username" required> <input type="username" class="form-control" id="inputUsername" name="username" placeholder="Username" required>
@ -114,10 +114,8 @@
</div> </div>
</div> </div>
</div> </div>
<script src="common.js"></script>
<script> <script>
var usernameEnabled = {{ .username }} window.validationStrings = {
var validationStrings = {
"length": { "length": {
"singular": "Must have at least {n} character", "singular": "Must have at least {n} character",
"plural": "Must have a least {n} characters" "plural": "Must have a least {n} characters"
@ -138,8 +136,9 @@
"singular": "Must have at least {n} special character", "singular": "Must have at least {n} special character",
"plural": "Must have at least {n} special characters" "plural": "Must have at least {n} special characters"
} }
} };
</script> </script>
<script src="form.js"></script> {{ template "form-base" .settings }}
</body> </body>
</html> </html>

7
go.mod
View File

@ -12,8 +12,8 @@ require (
github.com/gin-contrib/static v0.0.0-20200916080430-d45d9a37d28e github.com/gin-contrib/static v0.0.0-20200916080430-d45d9a37d28e
github.com/gin-gonic/gin v1.6.3 github.com/gin-gonic/gin v1.6.3
github.com/go-chi/chi v4.1.2+incompatible // indirect github.com/go-chi/chi v4.1.2+incompatible // indirect
github.com/go-openapi/spec v0.19.9 // indirect github.com/go-openapi/spec v0.19.10 // indirect
github.com/go-openapi/swag v0.19.9 // indirect github.com/go-openapi/swag v0.19.10 // indirect
github.com/go-playground/validator/v10 v10.4.0 // indirect github.com/go-playground/validator/v10 v10.4.0 // indirect
github.com/golang/protobuf v1.4.2 github.com/golang/protobuf v1.4.2
github.com/google/uuid v1.1.2 // indirect github.com/google/uuid v1.1.2 // indirect
@ -35,8 +35,7 @@ require (
github.com/ugorji/go v1.1.9 // indirect github.com/ugorji/go v1.1.9 // indirect
github.com/urfave/cli/v2 v2.2.0 // indirect github.com/urfave/cli/v2 v2.2.0 // indirect
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect
golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0 // indirect golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9 // indirect
golang.org/x/tools v0.0.0-20201017001424-6003fad69a88 // indirect
google.golang.org/protobuf v1.25.0 // indirect google.golang.org/protobuf v1.25.0 // indirect
gopkg.in/ini.v1 v1.61.0 gopkg.in/ini.v1 v1.61.0
gopkg.in/yaml.v2 v2.3.0 // indirect gopkg.in/yaml.v2 v2.3.0 // indirect

9
go.sum
View File

@ -69,12 +69,16 @@ github.com/go-openapi/spec v0.19.4 h1:ixzUSnHTd6hCemgtAJgluaTSGYpLNpJY4mA2DIkdOA
github.com/go-openapi/spec v0.19.4/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= github.com/go-openapi/spec v0.19.4/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
github.com/go-openapi/spec v0.19.9 h1:9z9cbFuZJ7AcvOHKIY+f6Aevb4vObNDkTEyoMfO7rAc= github.com/go-openapi/spec v0.19.9 h1:9z9cbFuZJ7AcvOHKIY+f6Aevb4vObNDkTEyoMfO7rAc=
github.com/go-openapi/spec v0.19.9/go.mod h1:vqK/dIdLGCosfvYsQV3WfC7N3TiZSnGY2RZKoFK7X28= github.com/go-openapi/spec v0.19.9/go.mod h1:vqK/dIdLGCosfvYsQV3WfC7N3TiZSnGY2RZKoFK7X28=
github.com/go-openapi/spec v0.19.10 h1:pcNevfYytLaOQuTju0wm6OqcqU/E/pRwuSGigrLTI28=
github.com/go-openapi/spec v0.19.10/go.mod h1:vqK/dIdLGCosfvYsQV3WfC7N3TiZSnGY2RZKoFK7X28=
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.9 h1:1IxuqvBUU3S2Bi4YC7tlP9SJF1gVpCvqN0T2Qof4azE= github.com/go-openapi/swag v0.19.9 h1:1IxuqvBUU3S2Bi4YC7tlP9SJF1gVpCvqN0T2Qof4azE=
github.com/go-openapi/swag v0.19.9/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= github.com/go-openapi/swag v0.19.9/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY=
github.com/go-openapi/swag v0.19.10 h1:A1SWXruroGP15P1sOiegIPbaKio+G9N5TwWTFaVPmAU=
github.com/go-openapi/swag v0.19.10/go.mod h1:Uc0gKkdR+ojzsEpjh39QChyu92vPgIr72POcgHMAgSY=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
@ -272,12 +276,15 @@ golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0 h1:wBouT66WTYFXdxfVdz9sVWARV
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0 h1:5kGOVHlq0euqwzgTC9Vu15p6fV1Wi0ArVi8da2urnVg= golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0 h1:5kGOVHlq0euqwzgTC9Vu15p6fV1Wi0ArVi8da2urnVg=
golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -325,6 +332,8 @@ golang.org/x/tools v0.0.0-20201008184944-d01b322e6f06 h1:w9ail9jFLaySAm61Zjhciu0
golang.org/x/tools v0.0.0-20201008184944-d01b322e6f06/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201008184944-d01b322e6f06/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20201017001424-6003fad69a88 h1:ZB1XYzdDo7c/O48jzjMkvIjnC120Z9/CwgDWhePjQdQ= golang.org/x/tools v0.0.0-20201017001424-6003fad69a88 h1:ZB1XYzdDo7c/O48jzjMkvIjnC120Z9/CwgDWhePjQdQ=
golang.org/x/tools v0.0.0-20201017001424-6003fad69a88/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201017001424-6003fad69a88/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9 h1:sEvmEcJVKBNUvgCUClbUQeHOAa9U0I2Ce1BooMvVCY4=
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

6
package-lock.json generated
View File

@ -50,9 +50,9 @@
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
}, },
"@types/jquery": { "@types/jquery": {
"version": "3.5.1", "version": "3.5.3",
"resolved": "https://registry.npm.taobao.org/@types/jquery/download/@types/jquery-3.5.1.tgz", "resolved": "https://registry.npm.taobao.org/@types/jquery/download/@types/jquery-3.5.3.tgz?cache=0&sync_timestamp=1602524936372&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fjquery%2Fdownload%2F%40types%2Fjquery-3.5.3.tgz",
"integrity": "sha1-zrsFes9QccQOQ58w6EDFejDUBsM=", "integrity": "sha1-rcxkfkxnW9nrrn+5gOnKddWO6Mc=",
"requires": { "requires": {
"@types/sizzle": "*" "@types/sizzle": "*"
} }

View File

@ -17,7 +17,7 @@
}, },
"homepage": "https://github.com/hrfee/jellyfin-accounts#readme", "homepage": "https://github.com/hrfee/jellyfin-accounts#readme",
"dependencies": { "dependencies": {
"@types/jquery": "^3.5.1", "@types/jquery": "^3.5.3",
"autoprefixer": "^9.8.5", "autoprefixer": "^9.8.5",
"bootstrap": "^5.0.0-alpha1", "bootstrap": "^5.0.0-alpha1",
"bootstrap4": "npm:bootstrap@^4.5.0", "bootstrap4": "npm:bootstrap@^4.5.0",

View File

@ -38,7 +38,7 @@ func (vd *Validator) validate(password string) map[string]bool {
} else if unicode.IsLower(c) { } else if unicode.IsLower(c) {
count["lowercase"] += 1 count["lowercase"] += 1
} else if unicode.IsNumber(c) { } else if unicode.IsNumber(c) {
count["numbers"] += 1 count["number"] += 1
} else { } else {
for _, s := range vd.specialChars { for _, s := range vd.specialChars {
if c == s { if c == s {

View File

@ -1,25 +1,17 @@
const checkCheckboxes = (): void => { import { checkCheckboxes, populateUsers, populateRadios } from "./modules/accounts.js";
const defaultsButton = document.getElementById('accountsTabSetDefaults'); import { _post, _get, _delete, rmAttr, addAttr } from "./modules/common.js";
const deleteButton = document.getElementById('accountsTabDelete'); import { populateProfiles } from "./modules/settings.js";
const checkboxes: NodeListOf<HTMLInputElement> = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]:checked'); import { Focus, Unfocus, createEl, storeDefaults } from "./modules/admin.js";
let checked = checkboxes.length;
if (checked == 0) { interface aWindow extends Window {
Unfocus(defaultsButton); changeEmail(icon: HTMLElement, id: string): void;
Unfocus(deleteButton);
} else {
Focus(defaultsButton);
Focus(deleteButton);
if (checked == 1) {
deleteButton.textContent = 'Delete User';
} else {
deleteButton.textContent = 'Delete Users';
}
}
} }
declare var window: aWindow;
const validateEmail = (email: string): boolean => /\S+@\S+\.\S+/.test(email); const validateEmail = (email: string): boolean => /\S+@\S+\.\S+/.test(email);
function changeEmail(icon: HTMLElement, id: string): void { window.changeEmail = (icon: HTMLElement, id: string): void => {
const iconContent = icon.outerHTML; const iconContent = icon.outerHTML;
icon.setAttribute('class', ''); icon.setAttribute('class', '');
const entry = icon.nextElementSibling as HTMLInputElement; const entry = icon.nextElementSibling as HTMLInputElement;
@ -79,84 +71,6 @@ function changeEmail(icon: HTMLElement, id: string): void {
icon.parentNode.appendChild(cross); icon.parentNode.appendChild(cross);
}; };
var jfUsers: Array<Object>;
function populateUsers(): void {
const acList = document.getElementById('accountsList');
acList.innerHTML = `
<div class="d-flex align-items-center">
<strong>Getting Users...</strong>
<div class="spinner-border ml-auto" role="status" aria-hidden="true"></div>
</div>
`;
Unfocus(acList.parentNode.querySelector('thead'));
const accountsList = document.createElement('tbody');
accountsList.id = 'accountsList';
const generateEmail = (id: string, name: string, email: string): string => {
let entry: HTMLDivElement = document.createElement('div');
entry.id = 'email_' + id;
let emailValue: string = email;
if (emailValue == undefined) {
emailValue = "";
}
entry.innerHTML = `
<i class="fa fa-edit d-inline-block icon-button" style="margin-right: 2%;" onclick="changeEmail(this, '${id}')"></i>
<input type="email" class="form-control-plaintext form-control-sm text-muted d-inline-block addressText" id="address_${id}" style="width: auto;" value="${emailValue}" readonly>
`;
return entry.outerHTML;
};
const template = (id: string, username: string, email: string, lastActive: string, admin: boolean): string => {
let isAdmin = "No";
if (admin) {
isAdmin = "Yes";
}
let fci = "form-check-input";
if (bsVersion != 5) {
fci = "";
}
return `
<td nowrap="nowrap" class="align-middle" scope="row"><input class="${fci}" type="checkbox" value="" id="select_${id}" onclick="checkCheckboxes();"></td>
<td nowrap="nowrap" class="align-middle">${username}</td>
<td nowrap="nowrap" class="align-middle">${generateEmail(id, name, email)}</td>
<td nowrap="nowrap" class="align-middle">${lastActive}</td>
<td nowrap="nowrap" class="align-middle">${isAdmin}</td>
`;
};
_get("/users", null, function (): void {
if (this.readyState == 4 && this.status == 200) {
jfUsers = this.response['users'];
for (const user of jfUsers) {
let tr = document.createElement('tr');
tr.innerHTML = template(user['id'], user['name'], user['email'], user['last_active'], user['admin']);
accountsList.appendChild(tr);
}
Focus(acList.parentNode.querySelector('thead'));
acList.replaceWith(accountsList);
}
});
}
function populateRadios(): void {
const radioList = document.getElementById('defaultUserRadios');
radioList.textContent = '';
let first = true;
for (const i in jfUsers) {
const user = jfUsers[i];
const radio = document.createElement('div');
radio.classList.add('form-check');
let checked = '';
if (first) {
checked = 'checked';
first = false;
}
radio.innerHTML = `
<input class="form-check-input" type="radio" name="defaultRadios" id="default_${user['id']}" ${checked}>
<label class="form-check-label" for="default_${user['id']}">${user['name']}</label>`;
radioList.appendChild(radio);
}
}
(<HTMLInputElement>document.getElementById('selectAll')).onclick = function (): void { (<HTMLInputElement>document.getElementById('selectAll')).onclick = function (): void {
const checkboxes: NodeListOf<HTMLInputElement> = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]'); const checkboxes: NodeListOf<HTMLInputElement> = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]');
for (let i = 0; i < checkboxes.length; i++) { for (let i = 0; i < checkboxes.length; i++) {
@ -217,18 +131,18 @@ function populateRadios(): void {
} }
setTimeout((): void => { setTimeout((): void => {
Unfocus(deleteButton); Unfocus(deleteButton);
deleteModal.hide(); window.Modals.delete.hide();
}, 4000); }, 4000);
} else { } else {
Unfocus(deleteButton); Unfocus(deleteButton);
deleteModal.hide() window.Modals.delete.hide()
} }
populateUsers(); populateUsers();
checkCheckboxes(); checkCheckboxes();
} }
}); });
}; };
deleteModal.show(); window.Modals.delete.show();
}; };
(<HTMLInputElement>document.getElementById('selectAll')).checked = false; (<HTMLInputElement>document.getElementById('selectAll')).checked = false;
@ -250,9 +164,9 @@ function populateRadios(): void {
populateProfiles(true); populateProfiles(true);
const profileSelect = document.getElementById('profileSelect') as HTMLSelectElement; const profileSelect = document.getElementById('profileSelect') as HTMLSelectElement;
profileSelect.textContent = ''; profileSelect.textContent = '';
for (let i = 0; i < availableProfiles.length; i++) { for (let i = 0; i < window.availableProfiles.length; i++) {
profileSelect.innerHTML += ` profileSelect.innerHTML += `
<option value="${availableProfiles[i]}" ${(i == 0) ? "selected" : ""}>${availableProfiles[i]}</option> <option value="${window.availableProfiles[i]}" ${(i == 0) ? "selected" : ""}>${window.availableProfiles[i]}</option>
`; `;
} }
document.getElementById('defaultsTitle').textContent = `Apply settings to ${userIDs.length} ${userString}`; document.getElementById('defaultsTitle').textContent = `Apply settings to ${userIDs.length} ${userString}`;
@ -266,7 +180,7 @@ function populateRadios(): void {
Unfocus(document.getElementById('defaultUserRadiosBox')); Unfocus(document.getElementById('defaultUserRadiosBox'));
Unfocus(document.getElementById('newProfileBox')); Unfocus(document.getElementById('newProfileBox'));
document.getElementById('storeDefaults').onclick = (): void => storeDefaults(userIDs); document.getElementById('storeDefaults').onclick = (): void => storeDefaults(userIDs);
userDefaultsModal.show(); window.Modals.userDefaults.show();
}; };
(<HTMLSelectElement>document.getElementById('defaultsSource')).addEventListener('change', function (): void { (<HTMLSelectElement>document.getElementById('defaultsSource')).addEventListener('change', function (): void {
@ -311,7 +225,7 @@ function populateRadios(): void {
rmAttr(button, 'btn-success'); rmAttr(button, 'btn-success');
addAttr(button, 'btn-primary'); addAttr(button, 'btn-primary');
button.textContent = ogText; button.textContent = ogText;
newUserModal.hide(); window.Modals.newUser.hide();
}, 1000); }, 1000);
populateUsers(); populateUsers();
} else { } else {
@ -338,11 +252,5 @@ function populateRadios(): void {
if (document.getElementById('newUserName') != null) { if (document.getElementById('newUserName') != null) {
(<HTMLInputElement>document.getElementById('newUserName')).value = ''; (<HTMLInputElement>document.getElementById('newUserName')).value = '';
} }
newUserModal.show(); window.Modals.newUser.show();
}; };

View File

@ -1,8 +1,19 @@
// Set in admin.html import { serializeForm, rmAttr, addAttr, _get, _post, _delete } from "./modules/common.js";
var cssFile: string; import { Focus, Unfocus } from "./modules/admin.js";
import { toggleCSS } from "./modules/animation.js";
import { populateUsers, checkCheckboxes } from "./modules/accounts.js";
import { generateInvites, addOptions, checkDuration } from "./modules/invites.js";
import { showSetting, openSettings } from "./modules/settings.js";
import { BS4 } from "./modules/bs4.js";
import { BS5 } from "./modules/bs5.js";
import "./accounts.js";
import "./settings.js";
const Focus = (el: HTMLElement): void => rmAttr(el, 'unfocused'); interface aWindow extends Window {
const Unfocus = (el: HTMLElement): void => addAttr(el, 'unfocused'); toClipboard(str: string): void;
}
declare var window: aWindow;
interface TabSwitcher { interface TabSwitcher {
els: Array<HTMLDivElement>; els: Array<HTMLDivElement>;
@ -35,27 +46,43 @@ const tabs: TabSwitcher = {
tabs.focus(1); tabs.focus(1);
}, },
settings: (): void => openSettings(document.getElementById('settingsSections'), document.getElementById('settingsContent'), (): void => { settings: (): void => openSettings(document.getElementById('settingsSections'), document.getElementById('settingsContent'), (): void => {
triggerTooltips(); window.BS.triggerTooltips();
showSetting("ui"); showSetting("ui");
tabs.focus(2); tabs.focus(2);
}) })
}; };
// for (let i = 0; i < tabs.els.length; i++) { window.bsVersion = window.bs5 ? 5 : 4
// tabs.tabButtons[i].onclick = (): void => tabs.focus(i);
// } if (window.bs5) {
window.BS = new BS5;
} else {
window.BS = new BS4;
window.BS.Compat();
}
window.Modals = {} as BSModals;
window.Modals.login = window.BS.newModal('login');
window.Modals.userDefaults = window.BS.newModal('userDefaults');
window.Modals.users = window.BS.newModal('users');
window.Modals.restart = window.BS.newModal('restartModal');
window.Modals.refresh = window.BS.newModal('refreshModal');
window.Modals.about = window.BS.newModal('aboutModal');
window.Modals.delete = window.BS.newModal('deleteModal');
window.Modals.newUser = window.BS.newModal('newUserModal');
tabs.tabButtons[0].onclick = tabs.invites; tabs.tabButtons[0].onclick = tabs.invites;
tabs.tabButtons[1].onclick = tabs.accounts; tabs.tabButtons[1].onclick = tabs.accounts;
tabs.tabButtons[2].onclick = tabs.settings; tabs.tabButtons[2].onclick = tabs.settings;
tabs.invites(); tabs.invites();
// Predefined colors for the theme button. // Predefined colors for the theme button.
var buttonColor: string = "custom"; var buttonColor: string = "custom";
if (cssFile.includes("jf")) { if (window.cssFile.includes("jf")) {
buttonColor = "rgb(255,255,255)"; buttonColor = "rgb(255,255,255)";
} else if (cssFile == ("bs" + bsVersion + ".css")) { } else if (window.cssFile == ("bs" + window.bsVersion + ".css")) {
buttonColor = "rgb(16,16,16)"; buttonColor = "rgb(16,16,16)";
} }
@ -70,20 +97,11 @@ if (buttonColor != "custom") {
document.getElementById('headerButtons').appendChild(switchButton); document.getElementById('headerButtons').appendChild(switchButton);
} }
var loginModal = createModal('login');
var userDefaultsModal = createModal('userDefaults');
var usersModal = createModal('users');
var restartModal = createModal('restartModal');
var refreshModal = createModal('refreshModal');
var aboutModal = createModal('aboutModal');
var deleteModal = createModal('deleteModal');
var newUserModal = createModal('newUserModal');
var availableProfiles: Array<string>; var availableProfiles: Array<string>;
window["token"] = ""; window["token"] = "";
function toClipboard(str: string): void { window.toClipboard = (str: string): void => {
const el = document.createElement('textarea') as HTMLTextAreaElement; const el = document.createElement('textarea') as HTMLTextAreaElement;
el.value = str; el.value = str;
el.readOnly = true; el.readOnly = true;
@ -123,7 +141,7 @@ function login(username: string, password: string, modal: boolean, button?: HTML
button.textContent = "Login"; button.textContent = "Login";
}, 4000); }, 4000);
} else { } else {
loginModal.show(); window.Modals.login.show();
} }
} else { } else {
const data = this.response; const data = this.response;
@ -137,7 +155,7 @@ function login(username: string, password: string, modal: boolean, button?: HTML
minutes.value = "30"; minutes.value = "30";
checkDuration(); checkDuration();
if (modal) { if (modal) {
loginModal.hide(); window.Modals.login.hide();
} }
Focus(document.getElementById('logoutButton')); Focus(document.getElementById('logoutButton'));
} }
@ -149,12 +167,6 @@ function login(username: string, password: string, modal: boolean, button?: HTML
req.send(); req.send();
} }
function createEl(html: string): HTMLElement {
let div = document.createElement('div') as HTMLDivElement;
div.innerHTML = html;
return div.firstElementChild as HTMLElement;
}
(document.getElementById('loginForm') as HTMLFormElement).onsubmit = function (): boolean { (document.getElementById('loginForm') as HTMLFormElement).onsubmit = function (): boolean {
window.token = ""; window.token = "";
const details = serializeForm('loginForm'); const details = serializeForm('loginForm');
@ -169,70 +181,11 @@ function createEl(html: string): HTMLElement {
return false; return false;
}; };
function storeDefaults(users: string | Array<string>): void {
// not sure if this does anything, but w/e
this.disabled = true;
this.innerHTML =
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
'Loading...';
const button = document.getElementById('storeDefaults') as HTMLButtonElement;
let data = { "homescreen": false };
if ((document.getElementById('defaultsSource') as HTMLSelectElement).value == 'profile') {
data["from"] = "profile";
data["profile"] = (document.getElementById('profileSelect') as HTMLSelectElement).value;
} else {
const radio = document.querySelector('input[name=defaultRadios]:checked') as HTMLInputElement
let id = radio.id.replace("default_", "");
data["from"] = "user";
data["id"] = id;
}
if (users != "all") {
data["apply_to"] = users;
}
if ((document.getElementById('storeDefaultHomescreen') as HTMLInputElement).checked) {
data["homescreen"] = true;
}
_post("/users/settings", data, function (): void {
if (this.readyState == 4) {
if (this.status == 200 || this.status == 204) {
button.textContent = "Success";
addAttr(button, "btn-success");
rmAttr(button, "btn-danger");
rmAttr(button, "btn-primary");
button.disabled = false;
setTimeout((): void => {
button.textContent = "Submit";
addAttr(button, "btn-primary");
rmAttr(button, "btn-success");
button.disabled = false;
userDefaultsModal.hide();
}, 1000);
} else {
if ("error" in this.response) {
button.textContent = this.response["error"];
} else if (("policy" in this.response) || ("homescreen" in this.response)) {
button.textContent = "Failed (check console)";
} else {
button.textContent = "Failed";
}
addAttr(button, "btn-danger");
rmAttr(button, "btn-primary");
setTimeout((): void => {
button.textContent = "Submit";
addAttr(button, "btn-primary");
rmAttr(button, "btn-danger");
button.disabled = false;
}, 1000);
}
}
});
}
generateInvites(true); generateInvites(true);
login("", "", false, null, (status: number): void => { login("", "", false, null, (status: number): void => {
if (!(status == 200 || status == 204)) { if (!(status == 200 || status == 204)) {
loginModal.show(); window.Modals.login.show();
} }
}); });

View File

@ -1,36 +0,0 @@
var bsVersion = 4;
const send_to_addess_enabled = document.getElementById('send_to_addess_enabled');
if (send_to_addess_enabled) {
send_to_addess_enabled.classList.remove("form-check-input");
}
const multiUseEnabled = document.getElementById('multiUseEnabled');
if (multiUseEnabled) {
multiUseEnabled.classList.remove("form-check-input");
}
function createModal(id: string, find?: boolean): any {
$(`#${id}`).on("shown.bs.modal", (): void => document.body.classList.add("modal-open"));
return {
show: function (): any {
const temp = ($(`#${id}`) as any).modal("show");
return temp;
},
hide: function (): any {
return ($(`#${id}`) as any).modal("hide");
}
};
}
function triggerTooltips(): void {
const checkboxes = [].slice.call(document.getElementById('settingsContent').querySelectorAll('input[type="checkbox"]'));
for (const i in checkboxes) {
checkboxes[i].click();
checkboxes[i].click();
}
const tooltips = [].slice.call(document.querySelectorAll('a[data-toggle="tooltip"]'));
tooltips.map((el: HTMLAnchorElement): any => {
return ($(el) as any).tooltip();
});
}

View File

@ -1,34 +0,0 @@
declare var bootstrap: any;
var bsVersion = 5;
function createModal(id: string, find?: boolean): any {
let modal: any;
if (find) {
modal = bootstrap.Modal.getInstance(document.getElementById(id));
} else {
modal = new bootstrap.Modal(document.getElementById(id));
}
document.getElementById(id).addEventListener('shown.bs.modal', (): void => document.body.classList.add("modal-open"));
return {
modal: modal,
show: function (): any {
const temp = this.modal.show();
return temp;
},
hide: function (): any { return this.modal.hide(); }
};
}
function triggerTooltips(): void {
const checkboxes = [].slice.call(document.getElementById('settingsContent').querySelectorAll('input[type="checkbox"]'));
for (const i in checkboxes) {
checkboxes[i].click();
checkboxes[i].click();
}
const tooltips = [].slice.call(document.querySelectorAll('a[data-toggle="tooltip"]'));
tooltips.map((el: HTMLAnchorElement): any => {
return new bootstrap.Tooltip(el);
});
}

View File

@ -1,3 +1,14 @@
import { serializeForm, _post, _get, _delete, addAttr, rmAttr } from "./modules/common.js";
import { BS5 } from "./modules/bs5.js";
import { BS4 } from "./modules/bs4.js";
interface formWindow extends Window {
usernameEnabled: boolean;
validationStrings: pwValStrings;
}
declare var window: formWindow;
interface pwValString { interface pwValString {
singular: string; singular: string;
plural: string; plural: string;
@ -7,9 +18,6 @@ interface pwValStrings {
length, uppercase, lowercase, number, special: pwValString; length, uppercase, lowercase, number, special: pwValString;
} }
var validationStrings: pwValStrings;
var bsVersion: number;
var defaultPwValStrings: pwValStrings = { var defaultPwValStrings: pwValStrings = {
length: { length: {
singular: "Must have at least {n} character", singular: "Must have at least {n} character",
@ -45,49 +53,30 @@ const toggleSpinner = (): void => {
} }
}; };
for (let key in validationStrings) { for (let key in window.validationStrings) {
if (validationStrings[key].singular == "" || !(validationStrings[key].plural.includes("{n}"))) { if (window.validationStrings[key].singular == "" || !(window.validationStrings[key].plural.includes("{n}"))) {
validationStrings[key].singular = defaultPwValStrings[key].singular; window.validationStrings[key].singular = defaultPwValStrings[key].singular;
} }
if (validationStrings[key].plural == "" || !(validationStrings[key].plural.includes("{n}"))) { if (window.validationStrings[key].plural == "" || !(window.validationStrings[key].plural.includes("{n}"))) {
validationStrings[key].plural = defaultPwValStrings[key].plural; window.validationStrings[key].plural = defaultPwValStrings[key].plural;
} }
let el = document.getElementById(key) as HTMLUListElement; let el = document.getElementById(key) as HTMLUListElement;
if (el) { if (el) {
const min: number = +el.getAttribute("min"); const min: number = +el.getAttribute("min");
let text = ""; let text = "";
if (min == 1) { if (min == 1) {
text = validationStrings[key].singular.replace("{n}", "1"); text = window.validationStrings[key].singular.replace("{n}", "1");
} else { } else {
text = validationStrings[key].plural.replace("{n}", min.toString()); text = window.validationStrings[key].plural.replace("{n}", min.toString());
} }
(document.getElementById(key).children[0] as HTMLDivElement).textContent = text; (document.getElementById(key).children[0] as HTMLDivElement).textContent = text;
} }
} }
interface Modal { window.BS = window.bs5 ? new BS5 : new BS4;
show: () => void; var successBox: BSModal = window.BS.newModal('successBox');;
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 code = window.location.href.split('/').pop();
var usernameEnabled: boolean;
(document.getElementById('accountForm') as HTMLFormElement).addEventListener('submit', (event: any): boolean => { (document.getElementById('accountForm') as HTMLFormElement).addEventListener('submit', (event: any): boolean => {
event.preventDefault(); event.preventDefault();
@ -98,7 +87,7 @@ var usernameEnabled: boolean;
toggleSpinner(); toggleSpinner();
let send: Object = serializeForm('accountForm'); let send: Object = serializeForm('accountForm');
send["code"] = code; send["code"] = code;
if (!usernameEnabled) { if (!window.usernameEnabled) {
send["email"] = send["username"]; send["email"] = send["username"];
} }
_post("/newUser", send, function (): void { _post("/newUser", send, function (): void {

View File

@ -1,297 +1,11 @@
// Actually defined by templating in admin.html, this is just to avoid errors from tsc. import { serializeForm, rmAttr, addAttr, _get, _post, _delete } from "./modules/common.js";
var notifications_enabled: any; import { generateInvites, checkDuration } from "./modules/invites.js";
interface Invite { interface aWindow extends Window {
code?: string; setProfile(el: HTMLElement): void;
expiresIn?: string;
empty: boolean;
remainingUses?: string;
email?: string;
usedBy?: Array<Array<string>>;
created?: string;
notifyExpiry?: boolean;
notifyCreation?: boolean;
profile?: string;
} }
const emptyInvite = (): Invite => { return { code: "None", empty: true } as Invite; } declare var window: aWindow;
function parseInvite(invite: Object): Invite {
let inv: Invite = { code: invite["code"], empty: false, };
if (invite["email"]) {
inv.email = invite["email"];
}
let time = ""
const f = ["days", "hours", "minutes"];
for (const i in f) {
if (invite[f[i]] != 0) {
time += `${invite[f[i]]}${f[i][0]} `;
}
}
inv.expiresIn = `Expires in ${time.slice(0, -1)}`;
if (invite["no-limit"]) {
inv.remainingUses = "∞";
} else if ("remaining-uses" in invite) {
inv.remainingUses = invite["remaining-uses"];
}
if ("used-by" in invite) {
inv.usedBy = invite["used-by"];
}
if ("created" in invite) {
inv.created = invite["created"];
}
if ("notify-expiry" in invite) {
inv.notifyExpiry = invite["notify-expiry"];
}
if ("notify-creation" in invite) {
inv.notifyCreation = invite["notify-creation"];
}
if ("profile" in invite) {
inv.profile = invite["profile"];
}
return inv;
}
function setNotify(el: HTMLElement): void {
let send = {};
let code: string;
let notifyType: string;
if (el.id.includes("Expiry")) {
code = el.id.replace("_notifyExpiry", "");
notifyType = "notify-expiry";
} else if (el.id.includes("Creation")) {
code = el.id.replace("_notifyCreation", "");
notifyType = "notify-creation";
}
send[code] = {};
send[code][notifyType] = (el as HTMLInputElement).checked;
_post("/invites/notify", send, function (): void {
if (this.readyState == 4 && this.status != 200) {
(el as HTMLInputElement).checked = !(el as HTMLInputElement).checked;
}
});
}
function genUsedBy(usedBy: Array<Array<string>>): string {
let uB = "";
if (usedBy && usedBy.length != 0) {
uB = `
<ul class="list-group list-group-flush">
<li class="list-group-item py-1">Users created:</li>
`;
for (const i in usedBy) {
uB += `
<li class="list-group-item py-1 disabled">
<div class="d-flex float-left">${usedBy[i][0]}</div>
<div class="d-flex float-right">${usedBy[i][1]}</div>
</li>
`;
}
uB += `</ul>`
}
return uB;
}
function addItem(invite: Invite): void {
const links = document.getElementById('invites');
const container = document.createElement('div') as HTMLDivElement;
container.id = invite.code;
const item = document.createElement('div') as HTMLDivElement;
item.classList.add('list-group-item', 'd-flex', 'justify-content-between', 'd-inline-block');
let link = "";
let innerHTML = `<a>None</a>`;
if (invite.empty) {
item.innerHTML = `
<div class="d-flex align-items-center font-monospace" style="width: 40%;">
${innerHTML}
</div>
`;
container.appendChild(item);
links.appendChild(container);
return;
}
link = window.location.href.split('#')[0] + "invite/" + invite.code;
innerHTML = `
<div class="d-flex align-items-center font-monospace" style="width: 40%;">
<a class="invite-link" href="${link}">${invite.code.replace(/-/g, '-')}</a>
<i class="fa fa-clipboard icon-button" onclick="toClipboard('${link}')" style="margin-right: 0.5rem; margin-left: 0.5rem;"></i>
`;
if (invite.email) {
let email = invite.email;
if (!invite.email.includes("Failed to send to")) {
email = `Sent to ${email}`;
}
innerHTML += `
<span class="text-muted" style="margin-left: 0.4rem; font-style: italic; font-size: 0.8rem;">${email}</span>
`;
}
innerHTML += `
</div>
<div style="text-align: right;">
<span id="${invite.code}_expiry" style="margin-right: 1rem;">${invite.expiresIn}</span>
<div style="display: inline-block;">
<button class="btn btn-outline-danger" onclick="deleteInvite('${invite.code}')">Delete</button>
<i class="fa fa-angle-down collapsed icon-button not-rotated" style="padding: 1rem; margin: -1rem -1rem -1rem 0;" data-toggle="collapse" aria-expanded="false" data-target="#${CSS.escape(invite.code)}_collapse" onclick="rotateButton(this)"></i>
</div>
</div>
`;
item.innerHTML = innerHTML;
container.appendChild(item);
let profiles = `
<label class="input-group-text" for="profile_${CSS.escape(invite.code)}">Profile: </label>
<select class="form-select" id="profile_${CSS.escape(invite.code)}" onchange="setProfile(this)">
<option value="NoProfile" selected>No Profile</option>
`;
for (const i in availableProfiles) {
let selected = "";
if (availableProfiles[i] == invite.profile) {
selected = "selected";
}
profiles += `<option value="${availableProfiles[i]}" ${selected}>${availableProfiles[i]}</option>`;
}
profiles += `</select>`;
let dateCreated: string;
if (invite.created) {
dateCreated = `<li class="list-group-item py-1">Created: ${invite.created}</li>`;
}
let middle: string;
if (notifications_enabled) {
middle = `
<div class="col" id="${CSS.escape(invite.code)}_notifyButtons">
<ul class="list-group list-group-flush">
Notify on:
<li class="list-group-item py-1 form-check">
<input class="form-check-input" type="checkbox" value="" id="${CSS.escape(invite.code)}_notifyExpiry" onclick="setNotify(this)" ${invite.notifyExpiry ? "checked" : ""}>
<label class="form-check-label" for="${CSS.escape(invite.code)}_notifyExpiry">Expiry</label>
</li>
<li class="list-group-item py-1 form-check">
<input class="form-check-input" type="checkbox" value="" id="${CSS.escape(invite.code)}_notifyCreation" onclick="setNotify(this)" ${invite.notifyCreation ? "checked" : ""}>
<label class="form-check-label" for="${CSS.escape(invite.code)}_notifyCreation">User creation</label>
</li>
</ul>
</div>
`;
}
let right: string = genUsedBy(invite.usedBy)
const dropdown = document.createElement('div') as HTMLDivElement;
dropdown.id = `${CSS.escape(invite.code)}_collapse`;
dropdown.classList.add("collapse");
dropdown.innerHTML = `
<div class="container row align-items-start card-body">
<div class="col">
<ul class="list-group list-group-flush">
<li class="input-group py-1">
${profiles}
</li>
${dateCreated}
<li class="list-group-item py-1" id="${CSS.escape(invite.code)}_remainingUses">Remaining uses: ${invite.remainingUses}</li>
</ul>
</div>
${middle}
<div class="col" id="${CSS.escape(invite.code)}_usersCreated">
${right}
</div>
</div>
`;
container.appendChild(dropdown);
links.appendChild(container);
}
function updateInvite(invite: Invite): void {
document.getElementById(invite.code + "_expiry").textContent = invite.expiresIn;
const remainingUses: any = document.getElementById(CSS.escape(invite.code) + "_remainingUses");
if (remainingUses) {
remainingUses.textContent = `Remaining uses: ${invite.remainingUses}`;
}
document.getElementById(CSS.escape(invite.code) + "_usersCreated").innerHTML = genUsedBy(invite.usedBy);
}
// delete invite from DOM
const hideInvite = (code: string): void => document.getElementById(CSS.escape(code)).remove();
// delete invite from jfa-go
const deleteInvite = (code: string): void => _delete("/invites", { "code": code }, function (): void {
if (this.readyState == 4) {
generateInvites();
}
});
function generateInvites(empty?: boolean): void {
if (empty) {
document.getElementById('invites').textContent = '';
addItem(emptyInvite());
return;
}
_get("/invites", null, function (): void {
if (this.readyState == 4) {
let data = this.response;
availableProfiles = data['profiles'];
const Profiles = document.getElementById('inviteProfile') as HTMLSelectElement;
let innerHTML = "";
for (let i = 0; i < availableProfiles.length; i++) {
const profile = availableProfiles[i];
innerHTML += `
<option value="${profile}" ${(i == 0) ? "selected" : ""}>${profile}</option>
`;
}
innerHTML += `
<option value="NoProfile" ${(availableProfiles.length == 0) ? "selected" : ""}>No Profile</option>
`;
Profiles.innerHTML = innerHTML;
if (data['invites'] == null || data['invites'].length == 0) {
document.getElementById('invites').textContent = '';
addItem(emptyInvite());
return;
}
let items = document.getElementById('invites').children;
for (const i in data['invites']) {
let match = false;
const inv = parseInvite(data['invites'][i]);
for (const x in items) {
if (items[x].id == inv.code) {
match = true;
updateInvite(inv);
break;
}
}
if (!match) {
addItem(inv);
}
}
// second pass to check for expired invites
items = document.getElementById('invites').children;
for (let i = 0; i < items.length; i++) {
let exists = false;
for (const x in data['invites']) {
if (items[i].id == data['invites'][x]['code']) {
exists = true;
break;
}
}
if (!exists) {
hideInvite(items[i].id);
}
}
}
});
}
const addOptions = (length: number, el: HTMLSelectElement): void => {
for (let v = 0; v <= length; v++) {
const opt = document.createElement('option');
opt.textContent = ""+v;
opt.value = ""+v;
el.appendChild(opt);
}
el.value = "0";
};
function fixCheckboxes(): void { function fixCheckboxes(): void {
const send_to_address: Array<HTMLInputElement> = [document.getElementById('send_to_address') as HTMLInputElement, document.getElementById('send_to_address_enabled') as HTMLInputElement]; const send_to_address: Array<HTMLInputElement> = [document.getElementById('send_to_address') as HTMLInputElement, document.getElementById('send_to_address_enabled') as HTMLInputElement];
@ -329,7 +43,6 @@ fixCheckboxes();
delete send['send_to_address']; delete send['send_to_address'];
delete send['send_to_address_enabled']; delete send['send_to_address_enabled'];
} }
console.log(send);
_post("/invites", send, function (): void { _post("/invites", send, function (): void {
if (this.readyState == 4) { if (this.readyState == 4) {
button.textContent = 'Generate'; button.textContent = 'Generate';
@ -340,9 +53,9 @@ fixCheckboxes();
return false; return false;
}; };
triggerTooltips(); window.BS.triggerTooltips();
function setProfile(select: HTMLSelectElement): void { window.setProfile= (select: HTMLSelectElement): void => {
if (!select.value) { if (!select.value) {
return; return;
} }
@ -362,16 +75,6 @@ function setProfile(select: HTMLSelectElement): void {
}); });
} }
function checkDuration(): void {
const boxVals: Array<number> = [+(document.getElementById("days") as HTMLSelectElement).value, +(document.getElementById("hours") as HTMLSelectElement).value, +(document.getElementById("minutes") as HTMLSelectElement).value];
const submit = document.getElementById('generateSubmit') as HTMLButtonElement;
if (boxVals.reduce((a: number, b: number): number => a + b) == 0) {
submit.disabled = true;
} else {
submit.disabled = false;
}
}
const nE: Array<string> = ["days", "hours", "minutes"]; const nE: Array<string> = ["days", "hours", "minutes"];
for (const i in nE) { for (const i in nE) {
document.getElementById(nE[i]).addEventListener("change", checkDuration); document.getElementById(nE[i]).addEventListener("change", checkDuration);

106
ts/modules/accounts.ts Normal file
View File

@ -0,0 +1,106 @@
import { _get, _post, _delete } from "../modules/common.js";
import { Focus, Unfocus } from "../modules/admin.js";
interface aWindow extends Window {
checkCheckboxes: () => void;
}
declare var window: aWindow;
export const checkCheckboxes = (): void => {
const defaultsButton = document.getElementById('accountsTabSetDefaults');
const deleteButton = document.getElementById('accountsTabDelete');
const checkboxes: NodeListOf<HTMLInputElement> = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]:checked');
let checked = checkboxes.length;
if (checked == 0) {
Unfocus(defaultsButton);
Unfocus(deleteButton);
} else {
Focus(defaultsButton);
Focus(deleteButton);
if (checked == 1) {
deleteButton.textContent = 'Delete User';
} else {
deleteButton.textContent = 'Delete Users';
}
}
}
window.checkCheckboxes = checkCheckboxes;
export function populateUsers(): void {
const acList = document.getElementById('accountsList');
acList.innerHTML = `
<div class="d-flex align-items-center">
<strong>Getting Users...</strong>
<div class="spinner-border ml-auto" role="status" aria-hidden="true"></div>
</div>
`;
Unfocus(acList.parentNode.querySelector('thead'));
const accountsList = document.createElement('tbody');
accountsList.id = 'accountsList';
const generateEmail = (id: string, name: string, email: string): string => {
let entry: HTMLDivElement = document.createElement('div');
entry.id = 'email_' + id;
let emailValue: string = email;
if (emailValue == undefined) {
emailValue = "";
}
entry.innerHTML = `
<i class="fa fa-edit d-inline-block icon-button" style="margin-right: 2%;" onclick="changeEmail(this, '${id}')"></i>
<input type="email" class="form-control-plaintext form-control-sm text-muted d-inline-block addressText" id="address_${id}" style="width: auto;" value="${emailValue}" readonly>
`;
return entry.outerHTML;
};
const template = (id: string, username: string, email: string, lastActive: string, admin: boolean): string => {
let isAdmin = "No";
if (admin) {
isAdmin = "Yes";
}
let fci = "form-check-input";
if (window.bsVersion != 5) {
fci = "";
}
return `
<td nowrap="nowrap" class="align-middle" scope="row"><input class="${fci}" type="checkbox" value="" id="select_${id}" onclick="checkCheckboxes();"></td>
<td nowrap="nowrap" class="align-middle">${username}</td>
<td nowrap="nowrap" class="align-middle">${generateEmail(id, name, email)}</td>
<td nowrap="nowrap" class="align-middle">${lastActive}</td>
<td nowrap="nowrap" class="align-middle">${isAdmin}</td>
`;
};
_get("/users", null, function (): void {
if (this.readyState == 4 && this.status == 200) {
window.jfUsers = this.response['users'];
for (const user of window.jfUsers) {
let tr = document.createElement('tr');
tr.innerHTML = template(user['id'], user['name'], user['email'], user['last_active'], user['admin']);
accountsList.appendChild(tr);
}
Focus(acList.parentNode.querySelector('thead'));
acList.replaceWith(accountsList);
}
});
}
export function populateRadios(): void {
const radioList = document.getElementById('defaultUserRadios');
radioList.textContent = '';
let first = true;
for (const i in window.jfUsers) {
const user = window.jfUsers[i];
const radio = document.createElement('div');
radio.classList.add('form-check');
let checked = '';
if (first) {
checked = 'checked';
first = false;
}
radio.innerHTML = `
<input class="form-check-input" type="radio" name="defaultRadios" id="default_${user['id']}" ${checked}>
<label class="form-check-label" for="default_${user['id']}">${user['name']}</label>`;
radioList.appendChild(radio);
}
}

68
ts/modules/admin.ts Normal file
View File

@ -0,0 +1,68 @@
import { rmAttr, addAttr, _post, _get, _delete } from "../modules/common.js";
export const Focus = (el: HTMLElement): void => rmAttr(el, 'unfocused');
export const Unfocus = (el: HTMLElement): void => addAttr(el, 'unfocused');
export function createEl(html: string): HTMLElement {
let div = document.createElement('div') as HTMLDivElement;
div.innerHTML = html;
return div.firstElementChild as HTMLElement;
}
export function storeDefaults(users: string | Array<string>): void {
const button = document.getElementById('storeDefaults') as HTMLButtonElement;
button.disabled = true;
button.innerHTML =
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
'Loading...';
let data = { "homescreen": false };
if ((document.getElementById('defaultsSource') as HTMLSelectElement).value == 'profile') {
data["from"] = "profile";
data["profile"] = (document.getElementById('profileSelect') as HTMLSelectElement).value;
} else {
const radio = document.querySelector('input[name=defaultRadios]:checked') as HTMLInputElement
let id = radio.id.replace("default_", "");
data["from"] = "user";
data["id"] = id;
}
if (users != "all") {
data["apply_to"] = users;
}
if ((document.getElementById('storeDefaultHomescreen') as HTMLInputElement).checked) {
data["homescreen"] = true;
}
_post("/users/settings", data, function (): void {
if (this.readyState == 4) {
if (this.status == 200 || this.status == 204) {
button.textContent = "Success";
addAttr(button, "btn-success");
rmAttr(button, "btn-danger");
rmAttr(button, "btn-primary");
button.disabled = false;
setTimeout((): void => {
button.textContent = "Submit";
addAttr(button, "btn-primary");
rmAttr(button, "btn-success");
button.disabled = false;
window.Modals.userDefaults.hide();
}, 1000);
} else {
if ("error" in this.response) {
button.textContent = this.response["error"];
} else if (("policy" in this.response) || ("homescreen" in this.response)) {
button.textContent = "Failed (check console)";
} else {
button.textContent = "Failed";
}
addAttr(button, "btn-danger");
rmAttr(button, "btn-primary");
setTimeout((): void => {
button.textContent = "Submit";
addAttr(button, "btn-primary");
rmAttr(button, "btn-danger");
button.disabled = false;
}, 1000);
}
}
});
}

View File

@ -1,3 +1,11 @@
import { rmAttr, addAttr } from "../modules/common.js";
interface aWindow extends Window {
rotateButton(el: HTMLElement): void;
}
declare var window: aWindow;
// Used for animation on theme change // Used for animation on theme change
const whichTransitionEvent = (): string => { const whichTransitionEvent = (): string => {
const el = document.createElement('fakeElement'); const el = document.createElement('fakeElement');
@ -26,7 +34,7 @@ const _toggleCSS = (): void => {
cssEl = 1; cssEl = 1;
remove = true remove = true
} }
let href: string = "bs" + bsVersion; let href: string = "bs" + window.bsVersion;
if (!els[cssEl].href.includes(href + "-jf")) { if (!els[cssEl].href.includes(href + "-jf")) {
href += "-jf"; href += "-jf";
} }
@ -41,8 +49,8 @@ const _toggleCSS = (): void => {
} }
// Toggles between light and dark themes, but runs animation if window small enough. // Toggles between light and dark themes, but runs animation if window small enough.
var buttonWidth = 0; window.buttonWidth = 0;
const toggleCSS = (el: HTMLElement): void => { export const toggleCSS = (el: HTMLElement): void => {
const switchToColor = window.getComputedStyle(document.body, null).backgroundColor; const switchToColor = window.getComputedStyle(document.body, null).backgroundColor;
// Max page width for animation to take place // Max page width for animation to take place
let maxWidth = 1500; let maxWidth = 1500;
@ -51,7 +59,7 @@ const toggleCSS = (el: HTMLElement): void => {
const radius = Math.sqrt(Math.pow(window.innerWidth, 2) + Math.pow(window.innerHeight, 2)); const radius = Math.sqrt(Math.pow(window.innerWidth, 2) + Math.pow(window.innerHeight, 2));
const currentRadius = el.getBoundingClientRect().width / 2; const currentRadius = el.getBoundingClientRect().width / 2;
const scale = radius / currentRadius; const scale = radius / currentRadius;
buttonWidth = +window.getComputedStyle(el, null).width; window.buttonWidth = +window.getComputedStyle(el, null).width;
document.body.classList.remove('smooth-transition'); document.body.classList.remove('smooth-transition');
el.style.transform = `scale(${scale})`; el.style.transform = `scale(${scale})`;
el.style.color = switchToColor; el.style.color = switchToColor;
@ -68,7 +76,7 @@ const toggleCSS = (el: HTMLElement): void => {
} }
}; };
const rotateButton = (el: HTMLElement): void => { window.rotateButton = (el: HTMLElement): void => {
if (el.classList.contains("rotated")) { if (el.classList.contains("rotated")) {
rmAttr(el, "rotated") rmAttr(el, "rotated")
addAttr(el, "not-rotated"); addAttr(el, "not-rotated");

45
ts/modules/bs4.ts Normal file
View File

@ -0,0 +1,45 @@
declare var $: any;
class Modal implements BSModal {
el: HTMLDivElement;
modal: any;
constructor(id: string, find?: boolean) {
this.el = document.getElementById(id) as HTMLDivElement;
this.modal = $(this.el) as any;
this.modal.on("shown.b.modal", (): void => document.body.classList.add('modal-open'));
};
show(): void { this.modal.modal("show"); };
hide(): void { this.modal.modal("hide"); };
}
export class BS4 implements Bootstrap {
triggerTooltips: tooltipTrigger = function (): void {
const checkboxes = [].slice.call(document.getElementById('settingsContent').querySelectorAll('input[type="checkbox"]'));
for (const i in checkboxes) {
checkboxes[i].click();
checkboxes[i].click();
}
const tooltips = [].slice.call(document.querySelectorAll('a[data-toggle="tooltip"]'));
tooltips.map((el: HTMLAnchorElement): any => {
return ($(el) as any).tooltip();
});
};
Compat(): void {
console.log('Fixing BS4 Compatability');
const send_to_address_enabled = document.getElementById('send_to_address_enabled');
if (send_to_address_enabled) {
send_to_address_enabled.classList.remove("form-check-input");
}
const multiUseEnabled = document.getElementById('multiUseEnabled');
if (multiUseEnabled) {
multiUseEnabled.classList.remove("form-check-input");
}
}
newModal: ModalConstructor = function (id: string, find?: boolean): BSModal {
return new Modal(id, find);
};
}

37
ts/modules/bs5.ts Normal file
View File

@ -0,0 +1,37 @@
declare var bootstrap: any;
class Modal implements BSModal {
el: HTMLDivElement;
modal: any;
constructor(id: string, find?: boolean) {
this.el = document.getElementById(id) as HTMLDivElement;
if (find) {
this.modal = bootstrap.Modal.getInstance(this.el);
} else {
this.modal = new bootstrap.Modal(this.el);
}
this.el.addEventListener('shown.bs.modal', (): void => document.body.classList.add("modal-open"));
};
show(): void { this.modal.show(); };
hide(): void { this.modal.hide(); };
}
export class BS5 implements Bootstrap {
triggerTooltips: tooltipTrigger = function (): void {
const checkboxes = [].slice.call(document.getElementById('settingsContent').querySelectorAll('input[type="checkbox"]'));
for (const i in checkboxes) {
checkboxes[i].click();
checkboxes[i].click();
}
const tooltips = [].slice.call(document.querySelectorAll('a[data-toggle="tooltip"]'));
tooltips.map((el: HTMLAnchorElement): any => {
return new bootstrap.Tooltip(el);
});
};
newModal: ModalConstructor = function (id: string, find?: boolean): BSModal {
return new Modal(id, find);
};
};

View File

@ -1,8 +1,6 @@
interface Window { declare var window: Window;
token: string;
}
function serializeForm(id: string): Object { export function serializeForm(id: string): Object {
const form = document.getElementById(id) as HTMLFormElement; const form = document.getElementById(id) as HTMLFormElement;
let formData = {}; let formData = {};
for (let i = 0; i < form.elements.length; i++) { for (let i = 0; i < form.elements.length; i++) {
@ -38,15 +36,15 @@ function serializeForm(id: string): Object {
return formData; return formData;
} }
const rmAttr = (el: HTMLElement, attr: string): void => { export const rmAttr = (el: HTMLElement, attr: string): void => {
if (el.classList.contains(attr)) { if (el.classList.contains(attr)) {
el.classList.remove(attr); el.classList.remove(attr);
} }
}; };
const addAttr = (el: HTMLElement, attr: string): void => el.classList.add(attr); export const addAttr = (el: HTMLElement, attr: string): void => el.classList.add(attr);
const _get = (url: string, data: Object, onreadystatechange: () => void): void => { export const _get = (url: string, data: Object, onreadystatechange: () => void): void => {
let req = new XMLHttpRequest(); let req = new XMLHttpRequest();
req.open("GET", url, true); req.open("GET", url, true);
req.responseType = 'json'; req.responseType = 'json';
@ -56,7 +54,7 @@ const _get = (url: string, data: Object, onreadystatechange: () => void): void =
req.send(JSON.stringify(data)); req.send(JSON.stringify(data));
}; };
const _post = (url: string, data: Object, onreadystatechange: () => void, response?: boolean): void => { export const _post = (url: string, data: Object, onreadystatechange: () => void, response?: boolean): void => {
let req = new XMLHttpRequest(); let req = new XMLHttpRequest();
req.open("POST", url, true); req.open("POST", url, true);
if (response) { if (response) {
@ -68,7 +66,7 @@ const _post = (url: string, data: Object, onreadystatechange: () => void, respon
req.send(JSON.stringify(data)); req.send(JSON.stringify(data));
}; };
function _delete(url: string, data: Object, onreadystatechange: () => void): void { export function _delete(url: string, data: Object, onreadystatechange: () => void): void {
let req = new XMLHttpRequest(); let req = new XMLHttpRequest();
req.open("DELETE", url, true); req.open("DELETE", url, true);
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":")); req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));

297
ts/modules/invites.ts Normal file
View File

@ -0,0 +1,297 @@
import { _get, _post, _delete } from "../modules/common.js";
interface aWindow extends Window {
setNotify(el: HTMLElement): void;
deleteInvite(code: string): void;
}
declare var window: aWindow;
const emptyInvite = (): Invite => { return { code: "None", empty: true } as Invite; }
function genUsedBy(usedBy: Array<Array<string>>): string {
let uB = "";
if (usedBy && usedBy.length != 0) {
uB = `
<ul class="list-group list-group-flush">
<li class="list-group-item py-1">Users created:</li>
`;
for (const i in usedBy) {
uB += `
<li class="list-group-item py-1 disabled">
<div class="d-flex float-left">${usedBy[i][0]}</div>
<div class="d-flex float-right">${usedBy[i][1]}</div>
</li>
`;
}
uB += `</ul>`
}
return uB;
}
function addItem(invite: Invite): void {
const links = document.getElementById('invites');
const container = document.createElement('div') as HTMLDivElement;
container.id = invite.code;
const item = document.createElement('div') as HTMLDivElement;
item.classList.add('list-group-item', 'd-flex', 'justify-content-between', 'd-inline-block');
let link = "";
let innerHTML = `<a>None</a>`;
if (invite.empty) {
item.innerHTML = `
<div class="d-flex align-items-center font-monospace" style="width: 40%;">
${innerHTML}
</div>
`;
container.appendChild(item);
links.appendChild(container);
return;
}
link = window.location.href.split('#')[0] + "invite/" + invite.code;
innerHTML = `
<div class="d-flex align-items-center font-monospace" style="width: 40%;">
<a class="invite-link" href="${link}">${invite.code.replace(/-/g, '-')}</a>
<i class="fa fa-clipboard icon-button" onclick="window.toClipboard('${link}')" style="margin-right: 0.5rem; margin-left: 0.5rem;"></i>
`;
if (invite.email) {
let email = invite.email;
if (!invite.email.includes("Failed to send to")) {
email = `Sent to ${email}`;
}
innerHTML += `
<span class="text-muted" style="margin-left: 0.4rem; font-style: italic; font-size: 0.8rem;">${email}</span>
`;
}
innerHTML += `
</div>
<div style="text-align: right;">
<span id="${invite.code}_expiry" style="margin-right: 1rem;">${invite.expiresIn}</span>
<div style="display: inline-block;">
<button class="btn btn-outline-danger" onclick="deleteInvite('${invite.code}')">Delete</button>
<i class="fa fa-angle-down collapsed icon-button not-rotated" style="padding: 1rem; margin: -1rem -1rem -1rem 0;" data-toggle="collapse" aria-expanded="false" data-target="#${CSS.escape(invite.code)}_collapse" onclick="window.rotateButton(this)"></i>
</div>
</div>
`;
item.innerHTML = innerHTML;
container.appendChild(item);
let profiles = `
<label class="input-group-text" for="profile_${CSS.escape(invite.code)}">Profile: </label>
<select class="form-select" id="profile_${CSS.escape(invite.code)}" onchange="window.setProfile(this)">
<option value="NoProfile" selected>No Profile</option>
`;
for (const i in window.availableProfiles) {
let selected = "";
if (window.availableProfiles[i] == invite.profile) {
selected = "selected";
}
profiles += `<option value="${window.availableProfiles[i]}" ${selected}>${window.availableProfiles[i]}</option>`;
}
profiles += `</select>`;
let dateCreated: string;
if (invite.created) {
dateCreated = `<li class="list-group-item py-1">Created: ${invite.created}</li>`;
}
let middle: string;
if (window.notifications_enabled) {
middle = `
<div class="col" id="${CSS.escape(invite.code)}_notifyButtons">
<ul class="list-group list-group-flush">
Notify on:
<li class="list-group-item py-1 form-check">
<input class="form-check-input" type="checkbox" value="" id="${CSS.escape(invite.code)}_notifyExpiry" onclick="setNotify(this)" ${invite.notifyExpiry ? "checked" : ""}>
<label class="form-check-label" for="${CSS.escape(invite.code)}_notifyExpiry">Expiry</label>
</li>
<li class="list-group-item py-1 form-check">
<input class="form-check-input" type="checkbox" value="" id="${CSS.escape(invite.code)}_notifyCreation" onclick="setNotify(this)" ${invite.notifyCreation ? "checked" : ""}>
<label class="form-check-label" for="${CSS.escape(invite.code)}_notifyCreation">User creation</label>
</li>
</ul>
</div>
`;
}
let right: string = genUsedBy(invite.usedBy)
const dropdown = document.createElement('div') as HTMLDivElement;
dropdown.id = `${CSS.escape(invite.code)}_collapse`;
dropdown.classList.add("collapse");
dropdown.innerHTML = `
<div class="container row align-items-start card-body">
<div class="col">
<ul class="list-group list-group-flush">
<li class="input-group py-1">
${profiles}
</li>
${dateCreated}
<li class="list-group-item py-1" id="${CSS.escape(invite.code)}_remainingUses">Remaining uses: ${invite.remainingUses}</li>
</ul>
</div>
${middle}
<div class="col" id="${CSS.escape(invite.code)}_usersCreated">
${right}
</div>
</div>
`;
container.appendChild(dropdown);
links.appendChild(container);
}
function parseInvite(invite: Object): Invite {
let inv: Invite = { code: invite["code"], empty: false, };
if (invite["email"]) {
inv.email = invite["email"];
}
let time = ""
const f = ["days", "hours", "minutes"];
for (const i in f) {
if (invite[f[i]] != 0) {
time += `${invite[f[i]]}${f[i][0]} `;
}
}
inv.expiresIn = `Expires in ${time.slice(0, -1)}`;
if (invite["no-limit"]) {
inv.remainingUses = "∞";
} else if ("remaining-uses" in invite) {
inv.remainingUses = invite["remaining-uses"];
}
if ("used-by" in invite) {
inv.usedBy = invite["used-by"];
}
if ("created" in invite) {
inv.created = invite["created"];
}
if ("notify-expiry" in invite) {
inv.notifyExpiry = invite["notify-expiry"];
}
if ("notify-creation" in invite) {
inv.notifyCreation = invite["notify-creation"];
}
if ("profile" in invite) {
inv.profile = invite["profile"];
}
return inv;
}
window.setNotify = (el: HTMLElement): void => {
let send = {};
let code: string;
let notifyType: string;
if (el.id.includes("Expiry")) {
code = el.id.replace("_notifyExpiry", "");
notifyType = "notify-expiry";
} else if (el.id.includes("Creation")) {
code = el.id.replace("_notifyCreation", "");
notifyType = "notify-creation";
}
send[code] = {};
send[code][notifyType] = (el as HTMLInputElement).checked;
_post("/invites/notify", send, function (): void {
if (this.readyState == 4 && this.status != 200) {
(el as HTMLInputElement).checked = !(el as HTMLInputElement).checked;
}
});
}
function updateInvite(invite: Invite): void {
document.getElementById(invite.code + "_expiry").textContent = invite.expiresIn;
const remainingUses: any = document.getElementById(CSS.escape(invite.code) + "_remainingUses");
if (remainingUses) {
remainingUses.textContent = `Remaining uses: ${invite.remainingUses}`;
}
document.getElementById(CSS.escape(invite.code) + "_usersCreated").innerHTML = genUsedBy(invite.usedBy);
}
// delete invite from DOM
const hideInvite = (code: string): void => document.getElementById(CSS.escape(code)).remove();
// delete invite from jfa-go
window.deleteInvite = (code: string): void => _delete("/invites", { "code": code }, function (): void {
if (this.readyState == 4) {
generateInvites();
}
});
export function generateInvites(empty?: boolean): void {
if (empty) {
document.getElementById('invites').textContent = '';
addItem(emptyInvite());
return;
}
_get("/invites", null, function (): void {
if (this.readyState == 4) {
let data = this.response;
window.availableProfiles = data['profiles'];
const Profiles = document.getElementById('inviteProfile') as HTMLSelectElement;
let innerHTML = "";
for (let i = 0; i < window.availableProfiles.length; i++) {
const profile = window.availableProfiles[i];
innerHTML += `
<option value="${profile}" ${(i == 0) ? "selected" : ""}>${profile}</option>
`;
}
innerHTML += `
<option value="NoProfile" ${(window.availableProfiles.length == 0) ? "selected" : ""}>No Profile</option>
`;
Profiles.innerHTML = innerHTML;
if (data['invites'] == null || data['invites'].length == 0) {
document.getElementById('invites').textContent = '';
addItem(emptyInvite());
return;
}
let items = document.getElementById('invites').children;
for (const i in data['invites']) {
let match = false;
const inv = parseInvite(data['invites'][i]);
for (const x in items) {
if (items[x].id == inv.code) {
match = true;
updateInvite(inv);
break;
}
}
if (!match) {
addItem(inv);
}
}
// second pass to check for expired invites
items = document.getElementById('invites').children;
for (let i = 0; i < items.length; i++) {
let exists = false;
for (const x in data['invites']) {
if (items[i].id == data['invites'][x]['code']) {
exists = true;
break;
}
}
if (!exists) {
hideInvite(items[i].id);
}
}
}
});
}
export const addOptions = (length: number, el: HTMLSelectElement): void => {
for (let v = 0; v <= length; v++) {
const opt = document.createElement('option');
opt.textContent = ""+v;
opt.value = ""+v;
el.appendChild(opt);
}
el.value = "0";
};
export function checkDuration(): void {
const boxVals: Array<number> = [+(document.getElementById("days") as HTMLSelectElement).value, +(document.getElementById("hours") as HTMLSelectElement).value, +(document.getElementById("minutes") as HTMLSelectElement).value];
const submit = document.getElementById('generateSubmit') as HTMLButtonElement;
if (boxVals.reduce((a: number, b: number): number => a + b) == 0) {
submit.disabled = true;
} else {
submit.disabled = false;
}
}

164
ts/modules/settings.ts Normal file
View File

@ -0,0 +1,164 @@
import { _get, _post, _delete, rmAttr, addAttr } from "../modules/common.js";
import { Focus, Unfocus } from "../modules/admin.js";
interface Profile {
Admin: boolean;
LibraryAccess: string;
FromUser: string;
}
export const populateProfiles = (noTable?: boolean): void => _get("/profiles", null, function (): void {
if (this.readyState == 4 && this.status == 200) {
const profileList = document.getElementById('profileList');
profileList.textContent = '';
window.availableProfiles = [this.response["default_profile"]];
for (let name in this.response["profiles"]) {
if (name != window.availableProfiles[0]) {
window.availableProfiles.push(name);
}
const reqProfile = this.response["profiles"][name];
if (!noTable && name != "default_profile") {
const profile: Profile = {
Admin: reqProfile["admin"],
LibraryAccess: reqProfile["libraries"],
FromUser: reqProfile["fromUser"]
};
profileList.innerHTML += `
<td nowrap="nowrap" class="align-middle"><strong>${name}</strong></td>
<td nowrap="nowrap" class="align-middle"><input class="${window.bs5 ? "form-check-input" : ""}" type="radio" name="defaultProfile" onclick="setDefaultProfile('${name}')" ${(name == window.availableProfiles[0]) ? "checked" : ""}></td>
<td nowrap="nowrap" class="align-middle">${profile.FromUser}</td>
<td nowrap="nowrap" class="align-middle">${profile.Admin ? "Yes" : "No"}</td>
<td nowrap="nowrap" class="align-middle">${profile.LibraryAccess}</td>
<td nowrap="nowrap" class="align-middle"><button class="btn btn-outline-danger" id="defaultProfile_${name}" onclick="deleteProfile('${name}')">Delete</button></td>
`;
}
}
}
});
export const openSettings = (settingsList: HTMLElement, settingsContent: HTMLElement, callback?: () => void): void => _get("/config", null, function (): void {
if (this.readyState == 4 && this.status == 200) {
settingsList.textContent = '';
window.config = this.response;
for (const i in window.config["order"]) {
const section: string = window.config["order"][i]
const sectionCollapse = document.createElement('div') as HTMLDivElement;
Unfocus(sectionCollapse);
sectionCollapse.id = section;
const title: string = window.config[section]["meta"]["name"];
const description: string = window.config[section]["meta"]["description"];
const entryListID: string = `${section}_entryList`;
// const footerID: string = `${section}_footer`;
sectionCollapse.innerHTML = `
<div class="card card-body">
<small class="text-muted">${description}</small>
<div class="${entryListID}">
</div>
</div>
`;
for (const x in config[section]["order"]) {
const entry: string = config[section]["order"][x];
if (entry == "meta") {
continue;
}
let entryName: string = window.config[section][entry]["name"];
let required = false;
if (window.config[section][entry]["required"]) {
entryName += ` <sup class="text-danger">*</sup>`;
required = true;
}
if (window.config[section][entry]["requires_restart"]) {
entryName += ` <sup class="text-danger">R</sup>`;
}
if ("description" in window.config[section][entry]) {
entryName +=`
<a class="text-muted" href="#" data-toggle="tooltip" data-placement="right" title="${window.config[section][entry]['description']}"><i class="fa fa-question-circle-o"></i></a>
`;
}
const entryValue: boolean | string = window.config[section][entry]["value"];
const entryType: string = window.config[section][entry]["type"];
const entryGroup = document.createElement('div');
if (entryType == "bool") {
entryGroup.classList.add("form-check");
entryGroup.innerHTML = `
<input class="form-check-input" type="checkbox" value="" id="${section}_${entry}" ${(entryValue as boolean) ? 'checked': ''} ${required ? 'required' : ''}>
<label class="form-check-label" for="${section}_${entry}">${entryName}</label>
`;
(entryGroup.querySelector('input[type=checkbox]') as HTMLInputElement).onclick = function (): void {
const me = this as HTMLInputElement;
for (const y in window.config["order"]) {
const sect: string = window.config["order"][y];
for (const z in window.config[sect]["order"]) {
const ent: string = window.config[sect]["order"][z];
if (`${sect}_${window.config[sect][ent]['depends_true']}` == me.id) {
(document.getElementById(`${sect}_${ent}`) as HTMLInputElement).disabled = !(me.checked);
} else if (`${sect}_${window.config[sect][ent]['depends_false']}` == me.id) {
(document.getElementById(`${sect}_${ent}`) as HTMLInputElement).disabled = me.checked;
}
}
}
};
} else if ((entryType == 'text') || (entryType == 'email') || (entryType == 'password') || (entryType == 'number')) {
entryGroup.classList.add("form-group");
entryGroup.innerHTML = `
<label for="${section}_${entry}">${entryName}</label>
<input type="${entryType}" class="form-control" id="${section}_${entry}" aria-describedby="${entry}" value="${entryValue}" ${required ? 'required' : ''}>
`;
} else if (entryType == 'select') {
entryGroup.classList.add("form-group");
const entryOptions: Array<string> = window.config[section][entry]["options"];
let innerGroup = `
<label for="${section}_${entry}">${entryName}</label>
<select class="form-control" id="${section}_${entry}" ${required ? 'required' : ''}>
`;
for (const z in entryOptions) {
const entryOption = entryOptions[z];
let selected: boolean = (entryOption == entryValue);
innerGroup += `
<option value="${entryOption}" ${selected ? 'selected' : ''}>${entryOption}</option>
`;
}
innerGroup += `</select>`;
entryGroup.innerHTML = innerGroup;
}
sectionCollapse.getElementsByClassName(entryListID)[0].appendChild(entryGroup);
}
settingsList.innerHTML += `
<button type="button" class="list-group-item list-group-item-action" id="${section}_button" onclick="showSetting('${section}')">${title}</button>
`;
settingsContent.appendChild(sectionCollapse);
}
if (callback) {
callback();
}
}
});
export function showSetting(id: string, runBefore?: () => void): void {
const els = document.getElementById('settingsLeft').querySelectorAll("button[type=button]:not(.static)") as NodeListOf<HTMLButtonElement>;
for (let i = 0; i < els.length; i++) {
const el = els[i];
if (el.id != `${id}_button`) {
rmAttr(el, "active");
}
const sectEl = document.getElementById(el.id.replace("_button", ""));
if (sectEl.id != id) {
Unfocus(sectEl);
}
}
addAttr(document.getElementById(`${id}_button`), "active");
const section = document.getElementById(id);
if (runBefore) {
runBefore();
}
Focus(section);
if (screen.width <= 1100) {
// ugly
setTimeout((): void => section.scrollIntoView(<ScrollIntoViewOptions>{ block: "center", behavior: "smooth" }), 200);
}
}

View File

@ -1,4 +1,7 @@
const ombiDefaultsModal = createModal('ombiDefaults'); import { _get, _post, _delete, rmAttr, addAttr } from "modules/common.js";
const ombiDefaultsModal = window.BS.newModal('ombiDefaults');
(document.getElementById('openOmbiDefaults') as HTMLButtonElement).onclick = function (): void { (document.getElementById('openOmbiDefaults') as HTMLButtonElement).onclick = function (): void {
let button = this as HTMLButtonElement; let button = this as HTMLButtonElement;
button.disabled = true; button.disabled = true;

View File

@ -1,9 +1,28 @@
var config: Object = {}; import { _post, _get, _delete, rmAttr, addAttr } from "./modules/common.js";
var modifiedConfig: Object = {}; import { generateInvites } from "./modules/invites.js";
import { populateRadios } from "./modules/accounts.js";
import { Focus, Unfocus } from "./modules/admin.js";
import { showSetting, populateProfiles } from "./modules/settings.js";
interface aWindow extends Window {
setDefaultProfile(name: string): void;
deleteProfile(name: string): void;
createProfile(): void;
showSetting(id: string, runBefore?: () => void): void;
config: Object;
modifiedConfig: Object;
}
declare var window: aWindow;
window.config = {};
window.modifiedConfig = {};
window.showSetting = showSetting;
function sendConfig(restart?: boolean): void { function sendConfig(restart?: boolean): void {
modifiedConfig["restart-program"] = restart; window.modifiedConfig["restart-program"] = restart;
_post("/config", modifiedConfig, function (): void { _post("/config", window.modifiedConfig, function (): void {
if (this.readyState == 4) { if (this.readyState == 4) {
const save = document.getElementById("settingsSave") as HTMLButtonElement const save = document.getElementById("settingsSave") as HTMLButtonElement
if (this.status == 200 || this.status == 204) { if (this.status == 200 || this.status == 204) {
@ -19,159 +38,22 @@ function sendConfig(restart?: boolean): void {
save.textContent = "Save"; save.textContent = "Save";
} }
if (restart) { if (restart) {
refreshModal.show(); window.Modals.refresh.show();
} }
} }
}); });
} }
(document.getElementById('openAbout') as HTMLButtonElement).onclick = (): void => { (document.getElementById('openAbout') as HTMLButtonElement).onclick = (): void => {
aboutModal.show(); window.Modals.about.show();
}; };
const openSettings = (settingsList: HTMLElement, settingsContent: HTMLElement, callback?: () => void): void => _get("/config", null, function (): void {
if (this.readyState == 4 && this.status == 200) {
settingsList.textContent = '';
config = this.response;
for (const i in config["order"]) {
const section: string = config["order"][i]
const sectionCollapse = document.createElement('div') as HTMLDivElement;
Unfocus(sectionCollapse);
sectionCollapse.id = section;
const title: string = config[section]["meta"]["name"];
const description: string = config[section]["meta"]["description"];
const entryListID: string = `${section}_entryList`;
// const footerID: string = `${section}_footer`;
sectionCollapse.innerHTML = `
<div class="card card-body">
<small class="text-muted">${description}</small>
<div class="${entryListID}">
</div>
</div>
`;
for (const x in config[section]["order"]) {
const entry: string = config[section]["order"][x];
if (entry == "meta") {
continue;
}
let entryName: string = config[section][entry]["name"];
let required = false;
if (config[section][entry]["required"]) {
entryName += ` <sup class="text-danger">*</sup>`;
required = true;
}
if (config[section][entry]["requires_restart"]) {
entryName += ` <sup class="text-danger">R</sup>`;
}
if ("description" in config[section][entry]) {
entryName +=`
<a class="text-muted" href="#" data-toggle="tooltip" data-placement="right" title="${config[section][entry]['description']}"><i class="fa fa-question-circle-o"></i></a>
`;
}
const entryValue: boolean | string = config[section][entry]["value"];
const entryType: string = config[section][entry]["type"];
const entryGroup = document.createElement('div');
if (entryType == "bool") {
entryGroup.classList.add("form-check");
entryGroup.innerHTML = `
<input class="form-check-input" type="checkbox" value="" id="${section}_${entry}" ${(entryValue as boolean) ? 'checked': ''} ${required ? 'required' : ''}>
<label class="form-check-label" for="${section}_${entry}">${entryName}</label>
`;
(entryGroup.querySelector('input[type=checkbox]') as HTMLInputElement).onclick = function (): void {
const me = this as HTMLInputElement;
for (const y in config["order"]) {
const sect: string = config["order"][y];
for (const z in config[sect]["order"]) {
const ent: string = config[sect]["order"][z];
if (`${sect}_${config[sect][ent]['depends_true']}` == me.id) {
(document.getElementById(`${sect}_${ent}`) as HTMLInputElement).disabled = !(me.checked);
} else if (`${sect}_${config[sect][ent]['depends_false']}` == me.id) {
(document.getElementById(`${sect}_${ent}`) as HTMLInputElement).disabled = me.checked;
}
}
}
};
} else if ((entryType == 'text') || (entryType == 'email') || (entryType == 'password') || (entryType == 'number')) {
entryGroup.classList.add("form-group");
entryGroup.innerHTML = `
<label for="${section}_${entry}">${entryName}</label>
<input type="${entryType}" class="form-control" id="${section}_${entry}" aria-describedby="${entry}" value="${entryValue}" ${required ? 'required' : ''}>
`;
} else if (entryType == 'select') {
entryGroup.classList.add("form-group");
const entryOptions: Array<string> = config[section][entry]["options"];
let innerGroup = `
<label for="${section}_${entry}">${entryName}</label>
<select class="form-control" id="${section}_${entry}" ${required ? 'required' : ''}>
`;
for (const z in entryOptions) {
const entryOption = entryOptions[z];
let selected: boolean = (entryOption == entryValue);
innerGroup += `
<option value="${entryOption}" ${selected ? 'selected' : ''}>${entryOption}</option>
`;
}
innerGroup += `</select>`;
entryGroup.innerHTML = innerGroup;
}
sectionCollapse.getElementsByClassName(entryListID)[0].appendChild(entryGroup);
}
settingsList.innerHTML += `
<button type="button" class="list-group-item list-group-item-action" id="${section}_button" onclick="showSetting('${section}')">${title}</button>
`;
settingsContent.appendChild(sectionCollapse);
}
if (callback) {
callback();
}
}
});
interface Profile {
Admin: boolean;
LibraryAccess: string;
FromUser: string;
}
(document.getElementById('profiles_button') as HTMLButtonElement).onclick = (): void => showSetting("profiles", populateProfiles); (document.getElementById('profiles_button') as HTMLButtonElement).onclick = (): void => showSetting("profiles", populateProfiles);
const populateProfiles = (noTable?: boolean): void => _get("/profiles", null, function (): void { window.setDefaultProfile = (name: string): void => _post("/profiles/default", { "name": name }, function (): void {
if (this.readyState == 4 && this.status == 200) {
const profileList = document.getElementById('profileList');
profileList.textContent = '';
availableProfiles = [this.response["default_profile"]];
for (let name in this.response["profiles"]) {
if (name != availableProfiles[0]) {
availableProfiles.push(name);
}
const reqProfile = this.response["profiles"][name];
if (!noTable && name != "default_profile") {
const profile: Profile = {
Admin: reqProfile["admin"],
LibraryAccess: reqProfile["libraries"],
FromUser: reqProfile["fromUser"]
};
profileList.innerHTML += `
<td nowrap="nowrap" class="align-middle"><strong>${name}</strong></td>
<td nowrap="nowrap" class="align-middle"><input class="${(bsVersion == 5) ? "form-check-input" : ""}" type="radio" name="defaultProfile" onclick="setDefaultProfile('${name}')" ${(name == availableProfiles[0]) ? "checked" : ""}></td>
<td nowrap="nowrap" class="align-middle">${profile.FromUser}</td>
<td nowrap="nowrap" class="align-middle">${profile.Admin ? "Yes" : "No"}</td>
<td nowrap="nowrap" class="align-middle">${profile.LibraryAccess}</td>
<td nowrap="nowrap" class="align-middle"><button class="btn btn-outline-danger" id="defaultProfile_${name}" onclick="deleteProfile('${name}')">Delete</button></td>
`;
}
}
}
});
const setDefaultProfile = (name: string): void => _post("/profiles/default", { "name": name }, function (): void {
if (this.readyState == 4) { if (this.readyState == 4) {
if (this.status != 200) { if (this.status != 200) {
(document.getElementById(`defaultProfile_${availableProfiles[0]}`) as HTMLInputElement).checked = true; (document.getElementById(`defaultProfile_${window.availableProfiles[0]}`) as HTMLInputElement).checked = true;
(document.getElementById(`defaultProfile_${name}`) as HTMLInputElement).checked = false; (document.getElementById(`defaultProfile_${name}`) as HTMLInputElement).checked = false;
} else { } else {
generateInvites(); generateInvites();
@ -179,7 +61,7 @@ const setDefaultProfile = (name: string): void => _post("/profiles/default", { "
} }
}); });
const deleteProfile = (name: string): void => _delete("/profiles", { "name": name }, function (): void { window.deleteProfile = (name: string): void => _delete("/profiles", { "name": name }, function (): void {
if (this.readyState == 4 && this.status == 200) { if (this.readyState == 4 && this.status == 200) {
populateProfiles(); populateProfiles();
} }
@ -187,7 +69,7 @@ const deleteProfile = (name: string): void => _delete("/profiles", { "name": nam
const createProfile = (): void => _get("/users", null, function (): void { const createProfile = (): void => _get("/users", null, function (): void {
if (this.readyState == 4 && this.status == 200) { if (this.readyState == 4 && this.status == 200) {
jfUsers = this.response["users"]; window.jfUsers = this.response["users"];
populateRadios(); populateRadios();
const submitButton = document.getElementById('storeDefaults') as HTMLButtonElement; const submitButton = document.getElementById('storeDefaults') as HTMLButtonElement;
submitButton.disabled = false; submitButton.disabled = false;
@ -205,10 +87,12 @@ const createProfile = (): void => _get("/users", null, function (): void {
Focus(document.getElementById('newProfileBox')); Focus(document.getElementById('newProfileBox'));
(document.getElementById('newProfileName') as HTMLInputElement).value = ''; (document.getElementById('newProfileName') as HTMLInputElement).value = '';
Focus(document.getElementById('defaultUserRadiosBox')); Focus(document.getElementById('defaultUserRadiosBox'));
userDefaultsModal.show(); window.Modals.userDefaults.show();
} }
}); });
window.createProfile = createProfile;
function storeProfile(): void { function storeProfile(): void {
this.disabled = true; this.disabled = true;
this.innerHTML = this.innerHTML =
@ -239,7 +123,7 @@ function storeProfile(): void {
addAttr(button, "btn-primary"); addAttr(button, "btn-primary");
rmAttr(button, "btn-success"); rmAttr(button, "btn-success");
button.disabled = false; button.disabled = false;
userDefaultsModal.hide(); window.Modals.userDefaults.hide();
}, 1000); }, 1000);
populateProfiles(); populateProfiles();
@ -265,41 +149,17 @@ function storeProfile(): void {
}); });
} }
function showSetting(id: string, runBefore?: () => void): void {
const els = document.getElementById('settingsLeft').querySelectorAll("button[type=button]:not(.static)") as NodeListOf<HTMLButtonElement>;
for (let i = 0; i < els.length; i++) {
const el = els[i];
if (el.id != `${id}_button`) {
rmAttr(el, "active");
}
const sectEl = document.getElementById(el.id.replace("_button", ""));
if (sectEl.id != id) {
Unfocus(sectEl);
}
}
addAttr(document.getElementById(`${id}_button`), "active");
const section = document.getElementById(id);
if (runBefore) {
runBefore();
}
Focus(section);
if (screen.width <= 1100) {
// ugly
setTimeout((): void => section.scrollIntoView(<ScrollIntoViewOptions>{ block: "center", behavior: "smooth" }), 200);
}
}
// (document.getElementById('openSettings') as HTMLButtonElement).onclick = (): void => openSettings(document.getElementById('settingsList'), document.getElementById('settingsList'), (): void => settingsModal.show()); // (document.getElementById('openSettings') as HTMLButtonElement).onclick = (): void => openSettings(document.getElementById('settingsList'), document.getElementById('settingsList'), (): void => settingsModal.show());
(document.getElementById('settingsSave') as HTMLButtonElement).onclick = function (): void { (document.getElementById('settingsSave') as HTMLButtonElement).onclick = function (): void {
modifiedConfig = {}; window.modifiedConfig = {};
const save = this as HTMLButtonElement; const save = this as HTMLButtonElement;
let restartSettingsChanged = false; let restartSettingsChanged = false;
let settingsChanged = false; let settingsChanged = false;
for (const i in config["order"]) { for (const i in window.config["order"]) {
const section = config["order"][i]; const section = window.config["order"][i];
for (const x in config[section]["order"]) { for (const x in window.config[section]["order"]) {
const entry = config[section]["order"][x]; const entry = window.config[section]["order"][x];
if (entry == "meta") { if (entry == "meta") {
continue; continue;
} }
@ -311,13 +171,13 @@ function showSetting(id: string, runBefore?: () => void): void {
} else { } else {
val = el.value.toString(); val = el.value.toString();
} }
if (val != config[section][entry]["value"].toString()) { if (val != window.config[section][entry]["value"].toString()) {
if (!(section in modifiedConfig)) { if (!(section in window.modifiedConfig)) {
modifiedConfig[section] = {}; window.modifiedConfig[section] = {};
} }
modifiedConfig[section][entry] = val; window.modifiedConfig[section][entry] = val;
settingsChanged = true; settingsChanged = true;
if (config[section][entry]["requires_restart"]) { if (window.config[section][entry]["requires_restart"]) {
restartSettingsChanged = true; restartSettingsChanged = true;
} }
} }
@ -333,7 +193,7 @@ function showSetting(id: string, runBefore?: () => void): void {
if (restartButton) { if (restartButton) {
restartButton.onclick = (): void => sendConfig(true); restartButton.onclick = (): void => sendConfig(true);
} }
restartModal.show(); window.Modals.restart.show();
} else if (settingsChanged) { } else if (settingsChanged) {
save.innerHTML = spinnerHTML; save.innerHTML = spinnerHTML;
sendConfig(); sendConfig();

View File

@ -3,6 +3,6 @@
"outDir": "../data/static", "outDir": "../data/static",
"target": "es6", "target": "es6",
"lib": ["dom", "es2017"], "lib": ["dom", "es2017"],
"types": ["jquery"] "typeRoots": ["./node_modules/@types", "./typings"]
} }
} }

61
ts/typings/d.ts Normal file
View File

@ -0,0 +1,61 @@
declare interface ModalConstructor {
(id: string, find?: boolean): BSModal;
}
declare interface BSModal {
el: HTMLDivElement;
modal: any;
show: () => void;
hide: () => void;
}
declare interface Window {
getComputedStyle(element: HTMLElement, pseudoElt: HTMLElement): any;
bsVersion: number;
bs5: boolean;
BS: Bootstrap;
Modals: BSModals;
cssFile: string;
availableProfiles: Array<any>;
jfUsers: Array<Object>;
notifications_enabled: boolean;
token: string;
buttonWidth: number;
}
declare interface tooltipTrigger {
(): void;
}
declare interface Bootstrap {
newModal: ModalConstructor;
triggerTooltips: tooltipTrigger;
Compat?(): void;
}
declare interface BSModals {
login: BSModal;
userDefaults: BSModal;
users: BSModal;
restart: BSModal;
refresh: BSModal;
about: BSModal;
delete: BSModal;
newUser: BSModal;
}
interface Invite {
code?: string;
expiresIn?: string;
empty: boolean;
remainingUses?: string;
email?: string;
usedBy?: Array<Array<string>>;
created?: string;
notifyExpiry?: boolean;
notifyCreation?: boolean;
profile?: string;
}
declare var config: Object;
declare var modifiedConfig: Object;

View File

@ -2,6 +2,7 @@ package main
import ( import (
"net/http" "net/http"
"strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -30,8 +31,10 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
// if app.checkInvite(code, false, "") { // if app.checkInvite(code, false, "") {
if _, ok := app.storage.invites[code]; ok { if _, ok := app.storage.invites[code]; ok {
email := app.storage.invites[code].Email email := app.storage.invites[code].Email
gc.HTML(http.StatusOK, "form.html", gin.H{ if strings.Contains(email, "Failed") {
"bs5": app.config.Section("ui").Key("bs5").MustBool(false), email = ""
}
gc.HTML(http.StatusOK, "form-loader.html", gin.H{
"cssFile": app.cssFile, "cssFile": app.cssFile,
"contactMessage": app.config.Section("ui").Key("contact_message").String(), "contactMessage": app.config.Section("ui").Key("contact_message").String(),
"helpMessage": app.config.Section("ui").Key("help_message").String(), "helpMessage": app.config.Section("ui").Key("help_message").String(),
@ -40,7 +43,10 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
"validate": app.config.Section("password_validation").Key("enabled").MustBool(false), "validate": app.config.Section("password_validation").Key("enabled").MustBool(false),
"requirements": app.validator.getCriteria(), "requirements": app.validator.getCriteria(),
"email": email, "email": email,
"settings": map[string]bool{
"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),
},
}) })
} else { } else {
gc.HTML(404, "invalidCode.html", gin.H{ gc.HTML(404, "invalidCode.html", gin.H{