mirror of
https://github.com/hrfee/jfa-go.git
synced 2026-01-19 00:57:37 +01:00
Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e35d0579c8 | ||
|
|
ea80d2cb78 | ||
|
|
f3c3b3ce76 | ||
|
|
fa96f21429 | ||
|
|
b6f3cd7c1f | ||
|
|
d64e98da37 | ||
|
|
ba601935b5 | ||
|
|
34135d645d | ||
|
|
47abf20e1d | ||
|
|
493f10fa36 | ||
|
|
8e45ecb214 | ||
|
|
d4a92adc65 | ||
|
|
c84ea17af4 | ||
|
|
0f4e77364b | ||
|
|
d64d5c194f | ||
|
|
95c9f4f42d | ||
|
|
a89dc40ff2 | ||
|
|
8089187b3e | ||
|
|
29775e2e75 | ||
|
|
9d62b70daa | ||
|
|
301f502052 | ||
|
|
2d6b1717db | ||
|
|
9abb177427 | ||
|
|
2f9965bcda | ||
|
|
82d07e423c | ||
|
|
8e6cf799cd | ||
|
|
8672d7dc18 | ||
|
|
5fd2e81fe4 | ||
|
|
a12678bd4d | ||
|
|
0e415020f7 | ||
|
|
a834aa30cf | ||
|
|
e3644e8fbf | ||
|
|
04198f3d49 | ||
|
|
8f7a65bebb | ||
|
|
1ef37f91b2 | ||
|
|
64c5badddd | ||
|
|
2e0519b183 | ||
|
|
9e739e79e7 | ||
|
|
2a2435ae11 | ||
|
|
04a4a4ca95 | ||
|
|
7628e5d71d | ||
|
|
9fec714da7 | ||
|
|
e104bd8362 | ||
|
|
64ad8ccf6e | ||
|
|
313b75bead | ||
|
|
9f6e405d1c | ||
|
|
84fb69d440 | ||
|
|
62543cd0be | ||
|
|
b6537cef65 | ||
|
|
544f5674e8 | ||
|
|
ce844e0574 | ||
|
|
9bd7fca95e | ||
|
|
89e70f6f7a | ||
|
|
6b25215768 | ||
|
|
903a61d0f2 | ||
|
|
49ef3dfcf0 | ||
|
|
2ab9b48f4b | ||
|
|
b6ceee508c | ||
|
|
32b8ed4aa2 | ||
|
|
73886fc037 | ||
|
|
c4acb43cb8 | ||
|
|
49b056f1d6 | ||
|
|
70cf706a82 | ||
|
|
7c247b0aae |
47
.drone.yml
Normal file
47
.drone.yml
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
name: jfa-go
|
||||
kind: pipeline
|
||||
type: docker
|
||||
|
||||
steps:
|
||||
- name: fetch
|
||||
image: docker:git
|
||||
commands:
|
||||
- git fetch --tags
|
||||
- name: release
|
||||
image: golang:latest
|
||||
environment:
|
||||
GITHUB_TOKEN:
|
||||
from_secret: github_token
|
||||
commands:
|
||||
- apt update -y
|
||||
- apt install build-essential python3-pip curl software-properties-common sed upx -y
|
||||
- (curl -sL https://deb.nodesource.com/setup_14.x | bash -)
|
||||
- apt install nodejs
|
||||
- curl -sL https://git.io/goreleaser | bash
|
||||
when:
|
||||
event: tag
|
||||
|
||||
---
|
||||
name: jfa-go-git
|
||||
kind: pipeline
|
||||
type: docker
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: golang:latest
|
||||
commands:
|
||||
- apt update -y
|
||||
- apt install build-essential python3-pip curl software-properties-common sed upx -y
|
||||
- (curl -sL https://deb.nodesource.com/setup_14.x | bash -)
|
||||
- apt install nodejs
|
||||
- curl -sL https://git.io/goreleaser > goreleaser.sh
|
||||
- chmod +x goreleaser.sh
|
||||
- ./goreleaser.sh --snapshot --skip-publish --rm-dist
|
||||
- wget https://builds.hrfee.pw/upload.py
|
||||
- pip3 install requests
|
||||
- bash -c 'python3 upload.py https://builds.hrfee.pw hrfee jfa-go ./dist/*.tar.gz'
|
||||
environment:
|
||||
BUILDRONE_KEY:
|
||||
from_secret: BUILDRONE_KEY
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -5,10 +5,16 @@ scss/*.css*
|
||||
scss/bs4/*.css*
|
||||
scss/bs5/*.css*
|
||||
data/static/*.css
|
||||
data/static/*.js
|
||||
data/static/*.js.map
|
||||
data/static/ts/
|
||||
data/static/modules/
|
||||
!data/static/setup.js
|
||||
data/config-base.json
|
||||
data/config-default.ini
|
||||
data/*.html
|
||||
data/*.txt
|
||||
data/docs/
|
||||
dist/*
|
||||
jfa-go
|
||||
build/
|
||||
@@ -16,3 +22,5 @@ pkg/
|
||||
old/
|
||||
version.go
|
||||
notes
|
||||
docs/*
|
||||
!docs/go.mod
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# This is an example goreleaser.yaml file with some sane defaults.
|
||||
# Make sure to check the documentation at http://goreleaser.com
|
||||
project_name: jfa-go
|
||||
release:
|
||||
github:
|
||||
@@ -8,15 +6,17 @@ release:
|
||||
name_template: "v{{.Version}}"
|
||||
before:
|
||||
hooks:
|
||||
# You may remove this if you don't use go modules.
|
||||
- go mod download
|
||||
- python3 config/fixconfig.py -i config/config-base.json -o data/config-base.json
|
||||
- python3 config/generate_ini.py -i config/config-base.json -o data/config-default.ini --version {{.Version}}
|
||||
- python3 config/generate_ini.py -i config/config-base.json -o data/config-default.ini
|
||||
- python3 -m pip install libsass
|
||||
- python3 scss/get_node_deps.py
|
||||
- python3 scss/compile.py -y
|
||||
- python3 mail/generate.py -y
|
||||
- npm install
|
||||
- python3 scss/compile.py
|
||||
- python3 mail/generate.py
|
||||
- python3 version.py {{.Version}} version.go
|
||||
- bash -c 'npx esbuild ts/*.ts ts/modules/*.ts --outdir=data/static --minify'
|
||||
- go get -u github.com/swaggo/swag/cmd/swag
|
||||
- swag init -g main.go
|
||||
builds:
|
||||
- dir: ./
|
||||
env:
|
||||
@@ -39,10 +39,11 @@ archives:
|
||||
- data/*
|
||||
- data/templates/*
|
||||
- data/static/*
|
||||
- data/static/modules/*
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-testing"
|
||||
name_template: "git-{{.ShortCommit}}"
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
|
||||
@@ -6,7 +6,7 @@ RUN apt update -y \
|
||||
&& apt install build-essential python3-pip curl software-properties-common sed upx -y \
|
||||
&& (curl -sL https://deb.nodesource.com/setup_14.x | bash -) \
|
||||
&& apt install nodejs \
|
||||
&& (cd /opt/build; make headless; make compress) \
|
||||
&& (cd /opt/build; make all; make compress) \
|
||||
&& sed -i 's#id="pwrJfPath" placeholder="Folder"#id="pwrJfPath" value="/jf" disabled#g' /opt/build/build/data/templates/setup.html
|
||||
|
||||
FROM golang:latest
|
||||
|
||||
60
Makefile
60
Makefile
@@ -1,40 +1,45 @@
|
||||
configuration:
|
||||
echo "Fixing config-base"
|
||||
$(info Fixing config-base)
|
||||
python3 config/fixconfig.py -i config/config-base.json -o data/config-base.json
|
||||
echo "Generating config-default.ini"
|
||||
python3 config/generate_ini.py -i config/config-base.json -o data/config-default.ini --version git
|
||||
$(info Generating config-default.ini)
|
||||
python3 config/generate_ini.py -i config/config-base.json -o data/config-default.ini
|
||||
|
||||
sass:
|
||||
echo "Getting libsass"
|
||||
$(info Getting libsass)
|
||||
python3 -m pip install libsass
|
||||
echo "Getting node dependencies"
|
||||
python3 scss/get_node_deps.py
|
||||
echo "Compiling sass"
|
||||
$(info Getting node dependencies)
|
||||
npm install
|
||||
$(info Compiling sass)
|
||||
python3 scss/compile.py
|
||||
|
||||
sass-headless:
|
||||
echo "Getting libsass"
|
||||
python3 -m pip install libsass
|
||||
echo "Getting node dependencies"
|
||||
python3 scss/get_node_deps.py
|
||||
echo "Compiling sass"
|
||||
python3 scss/compile.py -y
|
||||
|
||||
mail-headless:
|
||||
echo "Generating email html"
|
||||
python3 mail/generate.py -y
|
||||
|
||||
mail:
|
||||
echo "Generating email html"
|
||||
email:
|
||||
$(info Generating email html)
|
||||
python3 mail/generate.py
|
||||
|
||||
typescript:
|
||||
$(info Compiling typescript)
|
||||
npx esbuild ts/*.ts ts/modules/*.ts --outdir=data/static --minify
|
||||
-rm -r data/static/ts
|
||||
-rm -r data/static/typings
|
||||
-rm data/static/*.map
|
||||
|
||||
ts-debug:
|
||||
-npx tsc -p ts/ --sourceMap
|
||||
-rm -r data/static/ts
|
||||
-rm -r data/static/typings
|
||||
cp -r ts data/static/
|
||||
|
||||
swagger:
|
||||
go get github.com/swaggo/swag/cmd/swag
|
||||
swag init -g main.go
|
||||
|
||||
version:
|
||||
python3 version.py auto version.go
|
||||
|
||||
compile:
|
||||
echo "Downloading deps"
|
||||
$(info Downloading deps)
|
||||
go mod download
|
||||
echo "Building"
|
||||
$(info Building)
|
||||
mkdir -p build
|
||||
CGO_ENABLED=0 go build -o build/jfa-go *.go
|
||||
|
||||
@@ -42,14 +47,11 @@ compress:
|
||||
upx --lzma build/jfa-go
|
||||
|
||||
copy:
|
||||
echo "Copying data"
|
||||
$(info Copying data)
|
||||
cp -r data build/
|
||||
|
||||
install:
|
||||
cp -r build $(DESTDIR)/jfa-go
|
||||
|
||||
all: configuration sass mail version compile copy
|
||||
headless: configuration sass-headless mail-headless version compile copy
|
||||
|
||||
|
||||
|
||||
all: configuration sass email version typescript swagger compile copy
|
||||
debug: configuration sass email version ts-debug swagger compile copy
|
||||
|
||||
25
README.md
25
README.md
@@ -8,10 +8,11 @@ I chose to rewrite the python [jellyfin-accounts](https://github.com/hrfee/jelly
|
||||
* 🧑 Invite based account creation: Sends invites to your friends or family, and let them choose their own username and password without relying on you.
|
||||
* Send invites via a link and/or email
|
||||
* Granular control over invites: Validity period as well as number of uses can be specified.
|
||||
* Account defaults: Configure an example account to your liking, and its permissions, access rights and homescreen layout can be applied to all new users.
|
||||
* Account profiles: Assign settings profiles to invites so new users have your predefined permissions, homescreen layout, etc. applied to their account on creation.
|
||||
* Password validation: Ensure users choose a strong password.
|
||||
* 🔗 Ombi Integration: Automatically creates Ombi accounts for new users using their email address and login details, and your own defined set of permissions.
|
||||
* 📨 Email storage: Add your existing user's email addresses through the UI, and jfa-go will ask new users for them on account creation.
|
||||
* Account management: Apply settings to your users individually or en masse, and delete users, optionally sending them an email notification with a reason.
|
||||
* 📨 Email storage: Add your existing users email addresses through the UI, and jfa-go will ask new users for them on account creation.
|
||||
* Email addresses can optionally be used instead of usernames
|
||||
* 🔑 Password resets: When user's forget their passwords and request a change in Jellyfin, jfa-go reads the PIN from the created file and sends it straight to the user via email.
|
||||
* Notifications: Get notified when someone creates an account, or an invite expires.
|
||||
@@ -24,19 +25,19 @@ I chose to rewrite the python [jellyfin-accounts](https://github.com/hrfee/jelly
|
||||
|
||||
## Interface
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/hrfee/jellyfin-accounts/main/images/jfa.gif" width="100%"></img>
|
||||
<img src="https://github.com/hrfee/jfa-go/blob/main/images/demo.gif" width="100%"></img>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/hrfee/jellyfin-accounts/main/images/admin.png" width="48%" style="margin-right: 1.5%;" alt="Admin page"></img>
|
||||
<img src="https://raw.githubusercontent.com/hrfee/jellyfin-accounts/main/images/create.png" width="48%" style="margin-left: 1.5%;" alt="Account creation page"></img>
|
||||
<img src="https://github.com/hrfee/jfa-go/blob/main/images/invites.png" width="48%" style="margin-left: 1.5%;" alt="Invites tab"></img>
|
||||
<img src="https://github.com/hrfee/jfa-go/blob/main/images/accounts.png" width="48%" style="margin-right: 1.5%;" alt="Accounts tab"></img>
|
||||
</p>
|
||||
|
||||
#### Install
|
||||
|
||||
Available on the AUR as [jfa-go](https://aur.archlinux.org/packages/jfa-go/) or [jfa-go-git](https://aur.archlinux.org/packages/jfa-go-git/).
|
||||
|
||||
For other platforms, grab an archive from the release section for your platform, and extract `jfa-go` and `data` to the same directory.
|
||||
For other platforms, grab an archive from the release section for your platform (or nightly builds [here](https://builds.hrfee.pw/view/hrfee/jfa-go)), and extract `jfa-go` and `data` to the same directory.
|
||||
* For linux users, you can place them inside `/opt/jfa-go` and then run
|
||||
`sudo ln -s /opt/jfa-go/jfa-go /usr/bin/jfa-go` to place it in your PATH.
|
||||
|
||||
@@ -54,9 +55,9 @@ docker create \
|
||||
```
|
||||
|
||||
#### Build from source
|
||||
A Dockerfile is provided that creates an image built from source, but it's only suitable for those who will run jfa-go in docker.
|
||||
If you're using docker, a Dockerfile is provided that builds from source.
|
||||
|
||||
Full build instructions can be found [here](https://github.com/hrfee/jfa-go/wiki/Build).
|
||||
Otherwise, full build instructions can be found [here](https://github.com/hrfee/jfa-go/wiki/Build).
|
||||
|
||||
#### Usage
|
||||
Simply run `jfa-go` to start the application. A setup wizard will start on `localhost:8056` (or your own specified address). Upon completion, refresh the page.
|
||||
@@ -69,16 +70,20 @@ Usage of ./jfa-go:
|
||||
alternate path to config file. (default "~/.config/jfa-go/config.ini")
|
||||
-data string
|
||||
alternate path to data directory. (default "~/.config/jfa-go")
|
||||
-debug
|
||||
Enables debug logging and exposes pprof.
|
||||
-host string
|
||||
alternate address to host web ui on.
|
||||
-port int
|
||||
alternate port to host web ui on.
|
||||
-swagger
|
||||
Enable swagger at /swagger/index.html
|
||||
```
|
||||
|
||||
If you're switching from jellyfin-accounts, copy your existing `~/.jf-accounts` to:
|
||||
|
||||
* `XDG_CONFIG_DIR/jfa-go` (usually ~/.config) on \*nix systems,
|
||||
* `XDG_CONFIG_DIR/jfa-go` (usually ~/.config/jfa-go) on \*nix systems,
|
||||
* `%AppData%/jfa-go` on Windows,
|
||||
* `~/Library/Application Support/jfa-go` on macOS.
|
||||
|
||||
(*or specify config/data path with `-config/-data` respectively.*)
|
||||
(or specify config/data path with `-config/-data` respectively.)
|
||||
|
||||
254
auth.go
254
auth.go
@@ -43,28 +43,15 @@ func CreateToken(userId, jfId string) (string, string, error) {
|
||||
return token, refresh, nil
|
||||
}
|
||||
|
||||
func respond(code int, message string, gc *gin.Context) {
|
||||
resp := map[string]string{}
|
||||
if code == 200 || code == 204 {
|
||||
resp["response"] = message
|
||||
} else {
|
||||
resp["error"] = message
|
||||
}
|
||||
gc.JSON(code, resp)
|
||||
gc.Abort()
|
||||
}
|
||||
|
||||
// Check header for token
|
||||
func (app *appContext) authenticate(gc *gin.Context) {
|
||||
header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)
|
||||
if header[0] != "Basic" {
|
||||
app.debug.Println("Invalid authentication header")
|
||||
if header[0] != "Bearer" {
|
||||
app.debug.Println("Invalid authorization header")
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
auth, _ := base64.StdEncoding.DecodeString(header[1])
|
||||
creds := strings.SplitN(string(auth), ":", 2)
|
||||
token, err := jwt.Parse(creds[0], checkToken)
|
||||
token, err := jwt.Parse(string(header[1]), checkToken)
|
||||
if err != nil {
|
||||
app.debug.Printf("Auth denied: %s", err)
|
||||
respond(401, "Unauthorized", gc)
|
||||
@@ -110,137 +97,132 @@ func checkToken(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(os.Getenv("JFA_SECRET")), nil
|
||||
}
|
||||
|
||||
// getToken checks the header for a username and password, as well as checking the refresh cookie.
|
||||
func (app *appContext) getToken(gc *gin.Context) {
|
||||
type getTokenDTO struct {
|
||||
Token string `json:"token" example:"kjsdklsfdkljfsjsdfklsdfkldsfjdfskjsdfjklsdf"` // API token for use with everything else.
|
||||
}
|
||||
|
||||
// @Summary Grabs an API token using username & password.
|
||||
// @description Click the lock icon next to this, login with your normal jfa-go credentials. Click 'try it out', then 'execute' and an API Key will be returned, copy it (not including quotes). On any of the other routes, click the lock icon and set the API key as "Bearer `your api key`".
|
||||
// @Produce json
|
||||
// @Success 200 {object} getTokenDTO
|
||||
// @Failure 401 {object} stringResponse
|
||||
// @Router /token/login [get]
|
||||
// @tags Auth
|
||||
// @Security getTokenAuth
|
||||
func (app *appContext) getTokenLogin(gc *gin.Context) {
|
||||
app.info.Println("Token requested (login attempt)")
|
||||
header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)
|
||||
auth, _ := base64.StdEncoding.DecodeString(header[1])
|
||||
creds := strings.SplitN(string(auth), ":", 2)
|
||||
// check cookie first
|
||||
var userID, jfID string
|
||||
valid := false
|
||||
noLogin := false
|
||||
checkLogin := func() {
|
||||
if creds[0] == "" || creds[1] == "" {
|
||||
app.debug.Println("Auth denied: blank username/password")
|
||||
respond(401, "Unauthorized", gc)
|
||||
if creds[0] == "" || creds[1] == "" {
|
||||
app.debug.Println("Auth denied: blank username/password")
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
match := false
|
||||
for _, user := range app.users {
|
||||
if user.Username == creds[0] && user.Password == creds[1] {
|
||||
match = true
|
||||
app.debug.Println("Found existing user")
|
||||
userID = user.UserID
|
||||
break
|
||||
}
|
||||
}
|
||||
if !app.jellyfinLogin && !match {
|
||||
app.info.Println("Auth denied: Invalid username/password")
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
if !match {
|
||||
var status int
|
||||
var err error
|
||||
var user map[string]interface{}
|
||||
user, status, err = app.authJf.Authenticate(creds[0], creds[1])
|
||||
if status != 200 || err != nil {
|
||||
if status == 401 || status == 400 {
|
||||
app.info.Println("Auth denied: Invalid username/password (Jellyfin)")
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
app.err.Printf("Auth failed: Couldn't authenticate with Jellyfin (%d/%s)", status, err)
|
||||
respond(500, "Jellyfin error", gc)
|
||||
return
|
||||
}
|
||||
match := false
|
||||
for _, user := range app.users {
|
||||
if user.Username == creds[0] && user.Password == creds[1] {
|
||||
match = true
|
||||
app.debug.Println("Found existing user")
|
||||
userID = user.UserID
|
||||
break
|
||||
}
|
||||
}
|
||||
if !app.jellyfinLogin && !match {
|
||||
app.info.Println("Auth denied: Invalid username/password")
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
if !match {
|
||||
var status int
|
||||
var err error
|
||||
var user map[string]interface{}
|
||||
user, status, err = app.authJf.authenticate(creds[0], creds[1])
|
||||
if status != 200 || err != nil {
|
||||
if status == 401 || status == 400 {
|
||||
app.info.Println("Auth denied: Invalid username/password (Jellyfin)")
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
app.err.Printf("Auth failed: Couldn't authenticate with Jellyfin (%d/%s)", status, err)
|
||||
respond(500, "Jellyfin error", gc)
|
||||
jfID = user["Id"].(string)
|
||||
if app.config.Section("ui").Key("admin_only").MustBool(true) {
|
||||
if !user["Policy"].(map[string]interface{})["IsAdministrator"].(bool) {
|
||||
app.debug.Printf("Auth denied: Users \"%s\" isn't admin", creds[0])
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
jfID = user["Id"].(string)
|
||||
if app.config.Section("ui").Key("admin_only").MustBool(true) {
|
||||
if !user["Policy"].(map[string]interface{})["IsAdministrator"].(bool) {
|
||||
app.debug.Printf("Auth denied: Users \"%s\" isn't admin", creds[0])
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
}
|
||||
// New users are only added when using jellyfinLogin.
|
||||
userID = shortuuid.New()
|
||||
newUser := User{
|
||||
UserID: userID,
|
||||
}
|
||||
app.debug.Printf("Token generated for user \"%s\"", creds[0])
|
||||
app.users = append(app.users, newUser)
|
||||
}
|
||||
valid = true
|
||||
}
|
||||
checkCookie := func() {
|
||||
cookie, err := gc.Cookie("refresh")
|
||||
if err == nil && cookie != "" {
|
||||
for _, token := range app.invalidTokens {
|
||||
if cookie == token {
|
||||
if creds[0] == "" || creds[1] == "" {
|
||||
app.debug.Println("getToken denied: Invalid refresh token and no username/password provided")
|
||||
respond(401, "Unauthorized", gc)
|
||||
noLogin = true
|
||||
return
|
||||
}
|
||||
app.debug.Println("getToken: Invalid token but username/password provided")
|
||||
return
|
||||
}
|
||||
}
|
||||
token, err := jwt.Parse(cookie, checkToken)
|
||||
if err != nil {
|
||||
if creds[0] == "" || creds[1] == "" {
|
||||
app.debug.Println("getToken denied: Invalid refresh token and no username/password provided")
|
||||
respond(401, "Unauthorized", gc)
|
||||
noLogin = true
|
||||
return
|
||||
}
|
||||
app.debug.Println("getToken: Invalid token but username/password provided")
|
||||
return
|
||||
}
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
expiryUnix, err := strconv.ParseInt(claims["exp"].(string), 10, 64)
|
||||
if err != nil {
|
||||
if creds[0] == "" || creds[1] == "" {
|
||||
app.debug.Printf("getToken denied: Invalid token (%s) and no username/password provided", err)
|
||||
respond(401, "Unauthorized", gc)
|
||||
noLogin = true
|
||||
return
|
||||
}
|
||||
app.debug.Printf("getToken: Invalid token (%s) but username/password provided", err)
|
||||
return
|
||||
}
|
||||
expiry := time.Unix(expiryUnix, 0)
|
||||
if !(ok && token.Valid && claims["type"].(string) == "refresh" && expiry.After(time.Now())) {
|
||||
if creds[0] == "" || creds[1] == "" {
|
||||
app.debug.Printf("getToken denied: Invalid token (%s) and no username/password provided", err)
|
||||
respond(401, "Unauthorized", gc)
|
||||
noLogin = true
|
||||
return
|
||||
}
|
||||
app.debug.Printf("getToken: Invalid token (%s) but username/password provided", err)
|
||||
return
|
||||
}
|
||||
userID = claims["id"].(string)
|
||||
jfID = claims["jfid"].(string)
|
||||
valid = true
|
||||
// New users are only added when using jellyfinLogin.
|
||||
userID = shortuuid.New()
|
||||
newUser := User{
|
||||
UserID: userID,
|
||||
}
|
||||
app.debug.Printf("Token generated for user \"%s\"", creds[0])
|
||||
app.users = append(app.users, newUser)
|
||||
}
|
||||
checkCookie()
|
||||
if !valid && !noLogin {
|
||||
checkLogin()
|
||||
}
|
||||
if valid {
|
||||
token, refresh, err := CreateToken(userID, jfID)
|
||||
if err != nil {
|
||||
app.err.Printf("getToken failed: Couldn't generate token (%s)", err)
|
||||
respond(500, "Couldn't generate token", gc)
|
||||
return
|
||||
}
|
||||
gc.SetCookie("refresh", refresh, (3600 * 24), "/", gc.Request.URL.Hostname(), true, true)
|
||||
gc.JSON(200, map[string]string{"token": token})
|
||||
} else {
|
||||
gc.AbortWithStatus(401)
|
||||
token, refresh, err := CreateToken(userID, jfID)
|
||||
if err != nil {
|
||||
app.err.Printf("getToken failed: Couldn't generate token (%s)", err)
|
||||
respond(500, "Couldn't generate token", gc)
|
||||
return
|
||||
}
|
||||
gc.SetCookie("refresh", refresh, (3600 * 24), "/", gc.Request.URL.Hostname(), true, true)
|
||||
gc.JSON(200, getTokenDTO{token})
|
||||
}
|
||||
|
||||
// @Summary Grabs an API token using a refresh token from cookies.
|
||||
// @Produce json
|
||||
// @Success 200 {object} getTokenDTO
|
||||
// @Failure 401 {object} stringResponse
|
||||
// @Router /token/refresh [get]
|
||||
// @tags Auth
|
||||
func (app *appContext) getTokenRefresh(gc *gin.Context) {
|
||||
app.debug.Println("Token requested (refresh token)")
|
||||
cookie, err := gc.Cookie("refresh")
|
||||
if err != nil || cookie == "" {
|
||||
app.debug.Printf("getTokenRefresh denied: Couldn't get token: %s", err)
|
||||
respond(400, "Couldn't get token", gc)
|
||||
return
|
||||
}
|
||||
for _, token := range app.invalidTokens {
|
||||
if cookie == token {
|
||||
app.debug.Println("getTokenRefresh: Invalid token")
|
||||
respond(401, "Invalid token", gc)
|
||||
return
|
||||
}
|
||||
}
|
||||
token, err := jwt.Parse(cookie, checkToken)
|
||||
if err != nil {
|
||||
app.debug.Println("getTokenRefresh: Invalid token")
|
||||
respond(400, "Invalid token", gc)
|
||||
return
|
||||
}
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
expiryUnix, err := strconv.ParseInt(claims["exp"].(string), 10, 64)
|
||||
if err != nil {
|
||||
app.debug.Printf("getTokenRefresh: Invalid token expiry: %s", err)
|
||||
respond(401, "Invalid token", gc)
|
||||
return
|
||||
}
|
||||
expiry := time.Unix(expiryUnix, 0)
|
||||
if !(ok && token.Valid && claims["type"].(string) == "refresh" && expiry.After(time.Now())) {
|
||||
app.debug.Printf("getTokenRefresh: Invalid token: %s", err)
|
||||
respond(401, "Invalid token", gc)
|
||||
return
|
||||
}
|
||||
userID := claims["id"].(string)
|
||||
jfID := claims["jfid"].(string)
|
||||
jwt, refresh, err := CreateToken(userID, jfID)
|
||||
if err != nil {
|
||||
app.err.Printf("getTokenRefresh failed: Couldn't generate token (%s)", err)
|
||||
respond(500, "Couldn't generate token", gc)
|
||||
return
|
||||
}
|
||||
gc.SetCookie("refresh", refresh, (3600 * 24), "/", gc.Request.URL.Hostname(), true, true)
|
||||
gc.JSON(200, getTokenDTO{jwt})
|
||||
}
|
||||
|
||||
23
common/common.go
Normal file
23
common/common.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
// TimeoutHandler recovers from an http timeout.
|
||||
type TimeoutHandler func()
|
||||
|
||||
// NewTimeoutHandler returns a new Timeout handler.
|
||||
func NewTimeoutHandler(name, addr string, noFail bool) TimeoutHandler {
|
||||
return func() {
|
||||
if r := recover(); r != nil {
|
||||
out := fmt.Sprintf("Failed to authenticate with %s @ %s: Timed out", name, addr)
|
||||
if noFail {
|
||||
log.Print(out)
|
||||
} else {
|
||||
log.Fatalf(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
common/go.mod
Normal file
3
common/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module github.com/hrfee/jfa-go/common
|
||||
|
||||
go 1.15
|
||||
11
config.go
11
config.go
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
@@ -46,9 +47,11 @@ func (app *appContext) loadConfig() error {
|
||||
// if key.MustString("") == "" && key.Name() != "custom_css" {
|
||||
// key.SetValue(filepath.Join(app.data_path, (key.Name() + ".json")))
|
||||
// }
|
||||
key.SetValue(key.MustString(filepath.Join(app.data_path, (key.Name() + ".json"))))
|
||||
if key.Name() != "html_templates" {
|
||||
key.SetValue(key.MustString(filepath.Join(app.data_path, (key.Name() + ".json"))))
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"user_configuration", "user_displayprefs", "ombi_template"} {
|
||||
for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template"} {
|
||||
// if app.config.Section("files").Key(key).MustString("") == "" {
|
||||
// key.SetValue(filepath.Join(app.data_path, (key.Name() + ".json")))
|
||||
// }
|
||||
@@ -72,6 +75,10 @@ func (app *appContext) loadConfig() error {
|
||||
app.config.Section("deletion").Key("email_html").SetValue(app.config.Section("deletion").Key("email_html").MustString(filepath.Join(app.local_path, "deleted.html")))
|
||||
app.config.Section("deletion").Key("email_text").SetValue(app.config.Section("deletion").Key("email_text").MustString(filepath.Join(app.local_path, "deleted.txt")))
|
||||
|
||||
app.config.Section("jellyfin").Key("version").SetValue(VERSION)
|
||||
app.config.Section("jellyfin").Key("device").SetValue("jfa-go")
|
||||
app.config.Section("jellyfin").Key("device_id").SetValue(fmt.Sprintf("jfa-go-%s-%s", VERSION, COMMIT))
|
||||
|
||||
app.email = NewEmailer(app)
|
||||
|
||||
return nil
|
||||
|
||||
@@ -41,28 +41,15 @@
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "jfa-go",
|
||||
"description": "This and below settings will show on the Jellyfin dashboard when the program connects. You may as well leave them alone."
|
||||
"description": "The name of the client that will show up in the Jellyfin dashboard."
|
||||
},
|
||||
"version": {
|
||||
"name": "Version Number",
|
||||
"required": true,
|
||||
"cache_timeout": {
|
||||
"name": "User cache timeout (minutes)",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "{version}"
|
||||
},
|
||||
"device": {
|
||||
"name": "Device Name",
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "jfa-go"
|
||||
},
|
||||
"device_id": {
|
||||
"name": "Device ID",
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "jfa-go-{version}"
|
||||
"type": "number",
|
||||
"value": 30,
|
||||
"description": "Timeout of user cache in minutes. Set to 0 to disable."
|
||||
}
|
||||
},
|
||||
"ui": {
|
||||
@@ -70,6 +57,17 @@
|
||||
"name": "General",
|
||||
"description": "Settings related to the UI and program functionality."
|
||||
},
|
||||
"language": {
|
||||
"name": "Language",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "select",
|
||||
"options": [
|
||||
"en-us"
|
||||
],
|
||||
"value": "en-US",
|
||||
"description": "UI Language. Currently only implemented for account creation form. Submit a PR on github if you'd like to translate."
|
||||
},
|
||||
"theme": {
|
||||
"name": "Default Look",
|
||||
"required": false,
|
||||
@@ -177,10 +175,10 @@
|
||||
"bs5": {
|
||||
"name": "Use Bootstrap 5",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Use Bootstrap 5 (currently in alpha). This also removes the need for jQuery, so the page should load faster."
|
||||
"description": "Use the Bootstrap 5 Alpha. Looks better and removes the need for jQuery, so the page should load faster."
|
||||
}
|
||||
},
|
||||
"password_validation": {
|
||||
@@ -476,6 +474,14 @@
|
||||
"name": "SMTP (Email)",
|
||||
"description": "SMTP Server connection settings."
|
||||
},
|
||||
"username": {
|
||||
"name": "Username",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Username for SMTP. Leave blank to user send from address as username."
|
||||
},
|
||||
"encryption": {
|
||||
"name": "Encryption Method",
|
||||
"required": false,
|
||||
@@ -514,7 +520,7 @@
|
||||
"ombi": {
|
||||
"meta": {
|
||||
"name": "Ombi Integration",
|
||||
"description": "Connect to Ombi to automatically create a new user's account. You'll need to create an Ombi user template."
|
||||
"description": "Connect to Ombi to automatically create both Ombi and Jellyfin accounts for new users. You'll need to create a user template for this to work. Once enabled, refresh to see an option in settings for this."
|
||||
},
|
||||
"enabled": {
|
||||
"name": "Enabled",
|
||||
@@ -603,28 +609,36 @@
|
||||
"description": "Location of stored Ombi user template."
|
||||
},
|
||||
"user_template": {
|
||||
"name": "User Template",
|
||||
"name": "User Template (Deprecated)",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Location of stored user policy template (json)."
|
||||
"description": "Deprecated in favor of User Profiles. Location of stored user policy template (json)."
|
||||
},
|
||||
"user_configuration": {
|
||||
"name": "userConfiguration",
|
||||
"name": "userConfiguration (Deprecated)",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Location of stored user configuration template (used for setting homescreen layout) (json)"
|
||||
"description": "Deprecated in favor of User Profiles. Location of stored user configuration template (used for setting homescreen layout) (json)"
|
||||
},
|
||||
"user_displayprefs": {
|
||||
"name": "displayPreferences",
|
||||
"name": "displayPreferences (Deprecated)",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Location of stored displayPreferences template (also used for homescreen layout) (json)"
|
||||
"description": "Deprecated in favor of User Profiles. Location of stored displayPreferences template (also used for homescreen layout) (json)"
|
||||
},
|
||||
"user_profiles": {
|
||||
"name": "User Profiles",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Location of stored user profiles (encompasses template and configuration and displayprefs) (json)"
|
||||
},
|
||||
"custom_css": {
|
||||
"name": "Custom CSS",
|
||||
@@ -633,6 +647,14 @@
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Location of custom bootstrap CSS."
|
||||
},
|
||||
"html_templates": {
|
||||
"name": "Custom HTML Template Directory",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to directory containing custom versions of web ui pages. See wiki for more info."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,8 @@ import json
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-i", "--input", help="input config base from jf-accounts")
|
||||
parser.add_argument("-o", "--output", help="output ini")
|
||||
parser.add_argument("--version", help="version to include in file")
|
||||
|
||||
|
||||
def generate_ini(base_file, ini_file, version):
|
||||
def generate_ini(base_file, ini_file):
|
||||
"""
|
||||
Generates .ini file from config-base file.
|
||||
"""
|
||||
@@ -32,20 +27,16 @@ def generate_ini(base_file, ini_file, version):
|
||||
value = str(value)
|
||||
ini.set(section, entry, value)
|
||||
|
||||
ini["jellyfin"]["version"] = version
|
||||
ini["jellyfin"]["device_id"] = ini["jellyfin"]["device_id"].replace(
|
||||
"{version}", version
|
||||
)
|
||||
|
||||
with open(Path(ini_file), "w") as config_file:
|
||||
ini.write(config_file)
|
||||
return True
|
||||
|
||||
|
||||
args = parser.parse_args()
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-i", "--input", help="input config base from jf-accounts")
|
||||
parser.add_argument("-o", "--output", help="output ini")
|
||||
|
||||
version = "git"
|
||||
if args.version is not None:
|
||||
version = args.version
|
||||
args = parser.parse_args()
|
||||
|
||||
print(generate_ini(base_file=args.input, ini_file=args.output, version=version))
|
||||
print(generate_ini(base_file=args.input, ini_file=args.output))
|
||||
|
||||
@@ -1,744 +0,0 @@
|
||||
{
|
||||
"order": [
|
||||
"jellyfin",
|
||||
"ui",
|
||||
"password_validation",
|
||||
"email",
|
||||
"password_resets",
|
||||
"invite_emails",
|
||||
"notifications",
|
||||
"mailgun",
|
||||
"smtp",
|
||||
"ombi",
|
||||
"deletion",
|
||||
"files"
|
||||
],
|
||||
"jellyfin": {
|
||||
"order": [
|
||||
"username",
|
||||
"password",
|
||||
"server",
|
||||
"public_server",
|
||||
"client",
|
||||
"version",
|
||||
"device",
|
||||
"device_id"
|
||||
],
|
||||
"meta": {
|
||||
"name": "Jellyfin",
|
||||
"description": "Settings for connecting to Jellyfin"
|
||||
},
|
||||
"username": {
|
||||
"name": "Jellyfin Username",
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "username",
|
||||
"description": "It is recommended to create a limited admin account for this program."
|
||||
},
|
||||
"password": {
|
||||
"name": "Jellyfin Password",
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"type": "password",
|
||||
"value": "password"
|
||||
},
|
||||
"server": {
|
||||
"name": "Server address",
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "http://jellyfin.local:8096",
|
||||
"description": "Jellyfin server address. Can be public, or local for security purposes."
|
||||
},
|
||||
"public_server": {
|
||||
"name": "Public address",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "https://jellyf.in:443",
|
||||
"description": "Publicly accessible Jellyfin address for invite form. Leave blank to reuse the above address."
|
||||
},
|
||||
"client": {
|
||||
"name": "Client Name",
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "jfa-go",
|
||||
"description": "This and below settings will show on the Jellyfin dashboard when the program connects. You may as well leave them alone."
|
||||
},
|
||||
"version": {
|
||||
"name": "Version Number",
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "{version}"
|
||||
},
|
||||
"device": {
|
||||
"name": "Device Name",
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "jfa-go"
|
||||
},
|
||||
"device_id": {
|
||||
"name": "Device ID",
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "jfa-go-{version}"
|
||||
}
|
||||
},
|
||||
"ui": {
|
||||
"order": [
|
||||
"theme",
|
||||
"host",
|
||||
"port",
|
||||
"jellyfin_login",
|
||||
"admin_only",
|
||||
"username",
|
||||
"password",
|
||||
"email",
|
||||
"debug",
|
||||
"contact_message",
|
||||
"help_message",
|
||||
"success_message",
|
||||
"bs5"
|
||||
],
|
||||
"meta": {
|
||||
"name": "General",
|
||||
"description": "Settings related to the UI and program functionality."
|
||||
},
|
||||
"theme": {
|
||||
"name": "Default Look",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "select",
|
||||
"options": [
|
||||
"Bootstrap (Light)",
|
||||
"Jellyfin (Dark)",
|
||||
"Custom CSS"
|
||||
],
|
||||
"value": "Jellyfin (Dark)",
|
||||
"description": "Default appearance for all users."
|
||||
},
|
||||
"host": {
|
||||
"name": "Address",
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "0.0.0.0",
|
||||
"description": "Set 0.0.0.0 to run on localhost"
|
||||
},
|
||||
"port": {
|
||||
"name": "Port",
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"type": "number",
|
||||
"value": 8056
|
||||
},
|
||||
"jellyfin_login": {
|
||||
"name": "Use Jellyfin for authentication",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": true,
|
||||
"description": "Enable this to use Jellyfin users instead of the below username and pw."
|
||||
},
|
||||
"admin_only": {
|
||||
"name": "Allow admin users only",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"depends_true": "jellyfin_login",
|
||||
"type": "bool",
|
||||
"value": true,
|
||||
"description": "Allows only admin users on Jellyfin to access the admin page."
|
||||
},
|
||||
"username": {
|
||||
"name": "Web Username",
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"depends_false": "jellyfin_login",
|
||||
"type": "text",
|
||||
"value": "your username",
|
||||
"description": "Username for admin page (Leave blank if using jellyfin_login)"
|
||||
},
|
||||
"password": {
|
||||
"name": "Web Password",
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"depends_false": "jellyfin_login",
|
||||
"type": "password",
|
||||
"value": "your password",
|
||||
"description": "Password for admin page (Leave blank if using jellyfin_login)"
|
||||
},
|
||||
"email": {
|
||||
"name": "Admin email address",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_false": "jellyfin_login",
|
||||
"type": "text",
|
||||
"value": "example@example.com",
|
||||
"description": "Address to send notifications to (Leave blank if using jellyfin_login)"
|
||||
},
|
||||
"debug": {
|
||||
"name": "Debug logging",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Enables debug logging and exposes pprof as a route (Don't use in production!)"
|
||||
},
|
||||
"contact_message": {
|
||||
"name": "Contact message",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "Need help? contact me.",
|
||||
"description": "Displayed at bottom of all pages except admin"
|
||||
},
|
||||
"help_message": {
|
||||
"name": "Help message",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "Enter your details to create an account.",
|
||||
"description": "Displayed at top of invite form."
|
||||
},
|
||||
"success_message": {
|
||||
"name": "Success message",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "Your account has been created. Click below to continue to Jellyfin.",
|
||||
"description": "Displayed when a user creates an account"
|
||||
},
|
||||
"bs5": {
|
||||
"name": "Use Bootstrap 5",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Use Bootstrap 5 (currently in alpha). This also removes the need for jQuery, so the page should load faster."
|
||||
}
|
||||
},
|
||||
"password_validation": {
|
||||
"order": [
|
||||
"enabled",
|
||||
"min_length",
|
||||
"upper",
|
||||
"lower",
|
||||
"number",
|
||||
"special"
|
||||
],
|
||||
"meta": {
|
||||
"name": "Password Validation",
|
||||
"description": "Password validation (minimum length, etc.)"
|
||||
},
|
||||
"enabled": {
|
||||
"name": "Enabled",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "bool",
|
||||
"value": true
|
||||
},
|
||||
"min_length": {
|
||||
"name": "Minimum Length",
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "8"
|
||||
},
|
||||
"upper": {
|
||||
"name": "Minimum uppercase characters",
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "1"
|
||||
},
|
||||
"lower": {
|
||||
"name": "Minimum lowercase characters",
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "0"
|
||||
},
|
||||
"number": {
|
||||
"name": "Minimum number count",
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "1"
|
||||
},
|
||||
"special": {
|
||||
"name": "Minimum number of special characters",
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "0"
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
"order": [
|
||||
"no_username",
|
||||
"use_24h",
|
||||
"date_format",
|
||||
"message",
|
||||
"method",
|
||||
"address",
|
||||
"from"
|
||||
],
|
||||
"meta": {
|
||||
"name": "Email",
|
||||
"description": "General email settings. Ignore if not using email features."
|
||||
},
|
||||
"no_username": {
|
||||
"name": "Use email addresses as username",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "method",
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Use email address from invite form as username on Jellyfin."
|
||||
},
|
||||
"use_24h": {
|
||||
"name": "Use 24h time",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "method",
|
||||
"type": "bool",
|
||||
"value": true
|
||||
},
|
||||
"date_format": {
|
||||
"name": "Date format",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "method",
|
||||
"type": "text",
|
||||
"value": "%d/%m/%y",
|
||||
"description": "Date format used in emails. Follows datetime.strftime format."
|
||||
},
|
||||
"message": {
|
||||
"name": "Help message",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "method",
|
||||
"type": "text",
|
||||
"value": "Need help? contact me.",
|
||||
"description": "Message displayed at bottom of emails."
|
||||
},
|
||||
"method": {
|
||||
"name": "Email method",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "select",
|
||||
"options": [
|
||||
"smtp",
|
||||
"mailgun"
|
||||
],
|
||||
"value": "smtp",
|
||||
"description": "Method of sending email to use."
|
||||
},
|
||||
"address": {
|
||||
"name": "Sent from (address)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "method",
|
||||
"type": "email",
|
||||
"value": "jellyfin@jellyf.in",
|
||||
"description": "Address to send emails from"
|
||||
},
|
||||
"from": {
|
||||
"name": "Sent from (name)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "method",
|
||||
"type": "text",
|
||||
"value": "Jellyfin",
|
||||
"description": "The name of the sender"
|
||||
}
|
||||
},
|
||||
"password_resets": {
|
||||
"order": [
|
||||
"enabled",
|
||||
"watch_directory",
|
||||
"email_html",
|
||||
"email_text",
|
||||
"subject"
|
||||
],
|
||||
"meta": {
|
||||
"name": "Password Resets",
|
||||
"description": "Settings for the password reset handler."
|
||||
},
|
||||
"enabled": {
|
||||
"name": "Enabled",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": true,
|
||||
"description": "Enable to store provided email addresses, monitor Jellyfin directory for pw-resets, and send reset pins"
|
||||
},
|
||||
"watch_directory": {
|
||||
"name": "Jellyfin directory",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "/path/to/jellyfin",
|
||||
"description": "Path to the folder Jellyfin puts password-reset files."
|
||||
},
|
||||
"email_html": {
|
||||
"name": "Custom email (HTML)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to custom email html"
|
||||
},
|
||||
"email_text": {
|
||||
"name": "Custom email (plaintext)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to custom email in plain text"
|
||||
},
|
||||
"subject": {
|
||||
"name": "Email subject",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "Password Reset - Jellyfin",
|
||||
"description": "Subject of password reset emails."
|
||||
}
|
||||
},
|
||||
"invite_emails": {
|
||||
"order": [
|
||||
"enabled",
|
||||
"email_html",
|
||||
"email_text",
|
||||
"subject",
|
||||
"url_base"
|
||||
],
|
||||
"meta": {
|
||||
"name": "Invite emails",
|
||||
"description": "Settings for sending invites directly to users."
|
||||
},
|
||||
"enabled": {
|
||||
"name": "Enabled",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "bool",
|
||||
"value": true
|
||||
},
|
||||
"email_html": {
|
||||
"name": "Custom email (HTML)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to custom email HTML"
|
||||
},
|
||||
"email_text": {
|
||||
"name": "Custom email (plaintext)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to custom email in plain text"
|
||||
},
|
||||
"subject": {
|
||||
"name": "Email subject",
|
||||
"required": true,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "Invite - Jellyfin",
|
||||
"description": "Subject of invite emails."
|
||||
},
|
||||
"url_base": {
|
||||
"name": "URL Base",
|
||||
"required": true,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "http://accounts.jellyf.in:8056/invite",
|
||||
"description": "Base URL for jfa-go. This is necessary because using a reverse proxy means the program has no way of knowing the URL itself."
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"order": [
|
||||
"enabled",
|
||||
"expiry_html",
|
||||
"expiry_text",
|
||||
"created_html",
|
||||
"created_text"
|
||||
],
|
||||
"meta": {
|
||||
"name": "Notifications",
|
||||
"description": "Notification related settings."
|
||||
},
|
||||
"enabled": {
|
||||
"name": "Enabled",
|
||||
"required": "false",
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": true,
|
||||
"description": "Enabling adds optional toggles to invites to notify on expiry and user creation."
|
||||
},
|
||||
"expiry_html": {
|
||||
"name": "Expiry email (HTML)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to expiry notification email HTML."
|
||||
},
|
||||
"expiry_text": {
|
||||
"name": "Expiry email (Plaintext)",
|
||||
"required": false,
|
||||
"requires_restart": "false",
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to expiry notification email in plaintext."
|
||||
},
|
||||
"created_html": {
|
||||
"name": "User created email (HTML)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to user creation notification email HTML."
|
||||
},
|
||||
"created_text": {
|
||||
"name": "User created email (Plaintext)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to user creation notification email in plaintext."
|
||||
}
|
||||
},
|
||||
"mailgun": {
|
||||
"order": [
|
||||
"api_url",
|
||||
"api_key"
|
||||
],
|
||||
"meta": {
|
||||
"name": "Mailgun (Email)",
|
||||
"description": "Mailgun API connection settings"
|
||||
},
|
||||
"api_url": {
|
||||
"name": "API URL",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "https://api.mailgun.net..."
|
||||
},
|
||||
"api_key": {
|
||||
"name": "API Key",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "your api key"
|
||||
}
|
||||
},
|
||||
"smtp": {
|
||||
"order": [
|
||||
"encryption",
|
||||
"server",
|
||||
"port",
|
||||
"password"
|
||||
],
|
||||
"meta": {
|
||||
"name": "SMTP (Email)",
|
||||
"description": "SMTP Server connection settings."
|
||||
},
|
||||
"encryption": {
|
||||
"name": "Encryption Method",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "select",
|
||||
"options": [
|
||||
"ssl_tls",
|
||||
"starttls"
|
||||
],
|
||||
"value": "starttls",
|
||||
"description": "Your email provider should provide different ports for each encryption method. Generally 465 for ssl_tls, 587 for starttls."
|
||||
},
|
||||
"server": {
|
||||
"name": "Server address",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "smtp.jellyf.in",
|
||||
"description": "SMTP Server address."
|
||||
},
|
||||
"port": {
|
||||
"name": "Port",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "number",
|
||||
"value": 465
|
||||
},
|
||||
"password": {
|
||||
"name": "Password",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "password",
|
||||
"value": "smtp password"
|
||||
}
|
||||
},
|
||||
"ombi": {
|
||||
"order": [
|
||||
"enabled",
|
||||
"server",
|
||||
"api_key"
|
||||
],
|
||||
"meta": {
|
||||
"name": "Ombi Integration",
|
||||
"description": "Connect to Ombi to automatically create a new user's account. You'll need to create an Ombi user template."
|
||||
},
|
||||
"enabled": {
|
||||
"name": "Enabled",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Enable to create an Ombi account for new Jellyfin users"
|
||||
},
|
||||
"server": {
|
||||
"name": "URL",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "localhost:5000",
|
||||
"depends_true": "enabled",
|
||||
"description": "Ombi server URL, including http(s)://."
|
||||
},
|
||||
"api_key": {
|
||||
"name": "API Key",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"depends_true": "enabled",
|
||||
"description": "API Key. Get this from the first tab in Ombi settings."
|
||||
}
|
||||
},
|
||||
"deletion": {
|
||||
"order": [
|
||||
"subject",
|
||||
"email_html",
|
||||
"email_text"
|
||||
],
|
||||
"meta": {
|
||||
"name": "Account Deletion",
|
||||
"description": "Subject/email files for account deletion emails."
|
||||
},
|
||||
"subject": {
|
||||
"name": "Email subject",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "Your account was deleted - Jellyfin",
|
||||
"description": "Subject of account deletion emails."
|
||||
},
|
||||
"email_html": {
|
||||
"name": "Custom email (HTML)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to custom email html"
|
||||
},
|
||||
"email_text": {
|
||||
"name": "Custom email (plaintext)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to custom email in plain text"
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"order": [
|
||||
"invites",
|
||||
"emails",
|
||||
"ombi_template",
|
||||
"user_template",
|
||||
"user_configuration",
|
||||
"user_displayprefs",
|
||||
"custom_css"
|
||||
],
|
||||
"meta": {
|
||||
"name": "File Storage",
|
||||
"description": "Optional settings for changing storage locations."
|
||||
},
|
||||
"invites": {
|
||||
"name": "Invite Storage",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Location of stored invites (json)."
|
||||
},
|
||||
"emails": {
|
||||
"name": "Email Addresses",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Location of stored email addresses (json)."
|
||||
},
|
||||
"ombi_template": {
|
||||
"name": "Ombi user template",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Location of stored Ombi user template."
|
||||
},
|
||||
"user_template": {
|
||||
"name": "User Template",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Location of stored user policy template (json)."
|
||||
},
|
||||
"user_configuration": {
|
||||
"name": "userConfiguration",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Location of stored user configuration template (used for setting homescreen layout) (json)"
|
||||
},
|
||||
"user_displayprefs": {
|
||||
"name": "displayPreferences",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Location of stored displayPreferences template (also used for homescreen layout) (json)"
|
||||
},
|
||||
"custom_css": {
|
||||
"name": "Custom CSS",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Location of custom bootstrap CSS."
|
||||
}
|
||||
}
|
||||
}
|
||||
41
data/lang/form/en-us.json
Normal file
41
data/lang/form/en-us.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "English (US)"
|
||||
},
|
||||
"strings": {
|
||||
"pageTitle": "Create Jellyfin Account",
|
||||
"createAccountHeader": "Create Account",
|
||||
"accountDetails": "Details",
|
||||
"emailAddress": "Email",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"reEnterPassword": "Re-enter Password",
|
||||
"reEnterPasswordInvalid": "Passwords are not the same.",
|
||||
"createAccountButton": "Create Account",
|
||||
"passwordRequirementsHeader": "Password Requirements",
|
||||
"successHeader": "Success!",
|
||||
"successContinueButton": "Continue",
|
||||
"validationStrings": {
|
||||
"length": {
|
||||
"singular": "Must have at least {n} character",
|
||||
"plural": "Must have a least {n} characters"
|
||||
},
|
||||
"uppercase": {
|
||||
"singular": "Must have at least {n} uppercase character",
|
||||
"plural": "Must have at least {n} uppercase characters"
|
||||
},
|
||||
"lowercase": {
|
||||
"singular": "Must have at least {n} lowercase character",
|
||||
"plural": "Must have at least {n} lowercase characters"
|
||||
},
|
||||
"number": {
|
||||
"singular": "Must have at least {n} number",
|
||||
"plural": "Must have at least {n} numbers"
|
||||
},
|
||||
"special": {
|
||||
"singular": "Must have at least {n} special character",
|
||||
"plural": "Must have at least {n} special characters"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
data/lang/form/fr-fr.json
Normal file
42
data/lang/form/fr-fr.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Francais (FR)",
|
||||
"author": "https://github.com/Killianbe"
|
||||
},
|
||||
"strings": {
|
||||
"pageTitle": "Créer un compte Jellyfin",
|
||||
"createAccountHeader": "Création du compte",
|
||||
"accountDetails": "Détails",
|
||||
"emailAddress": "Email",
|
||||
"username": "Pseudo",
|
||||
"password": "Mot de passe",
|
||||
"reEnterPassword": "Confirmez mot de passe",
|
||||
"reEnterPasswordInvalid": "Les mots de passe ne correspondent pas.",
|
||||
"createAccountButton": "Créer le compte",
|
||||
"passwordRequirementsHeader": "Mot de passe requis",
|
||||
"successHeader": "Succes!",
|
||||
"successContinueButton": "Continuer",
|
||||
"validationStrings": {
|
||||
"length": {
|
||||
"singular": "Doit avoir au moins {n} caractère",
|
||||
"plural": "Doit avoir au moins {n} caractères"
|
||||
},
|
||||
"uppercase": {
|
||||
"singular": "Doit avoir au moins {n} caractère majuscule",
|
||||
"plural": "Must have at least {n} caractères majuscules"
|
||||
},
|
||||
"lowercase": {
|
||||
"singular": "Doit avoir au moins {n} caractère minuscule",
|
||||
"plural": "Doit avoir au moins {n} caractères minuscules"
|
||||
},
|
||||
"number": {
|
||||
"singular": "Doit avoir au moins {n} nombre",
|
||||
"plural": "Doit avoir au moins {n} nombres"
|
||||
},
|
||||
"special": {
|
||||
"singular": "Doit avoir au moins {n} caractère spécial",
|
||||
"plural": "Doit avoir au moins {n} caractères spéciaux"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,356 +0,0 @@
|
||||
document.getElementById('selectAll').onclick = function() {
|
||||
const checkboxes = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]');
|
||||
for (check of checkboxes) {
|
||||
check.checked = this.checked;
|
||||
}
|
||||
checkCheckboxes();
|
||||
};
|
||||
|
||||
function checkCheckboxes() {
|
||||
const defaultsButton = document.getElementById('accountsTabSetDefaults');
|
||||
const deleteButton = document.getElementById('accountsTabDelete');
|
||||
const checkboxes = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]');
|
||||
let checked = 0;
|
||||
for (check of checkboxes) {
|
||||
if (check.checked) {
|
||||
checked++;
|
||||
}
|
||||
}
|
||||
if (checked == 0) {
|
||||
defaultsButton.classList.add('unfocused');
|
||||
deleteButton.classList.add('unfocused');
|
||||
} else {
|
||||
if (defaultsButton.classList.contains('unfocused')) {
|
||||
defaultsButton.classList.remove('unfocused');
|
||||
}
|
||||
if (deleteButton.classList.contains('unfocused')) {
|
||||
deleteButton.classList.remove('unfocused');
|
||||
}
|
||||
if (checked == 1) {
|
||||
deleteButton.textContent = 'Delete User';
|
||||
} else {
|
||||
deleteButton.textContent = 'Delete Users';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('deleteModalNotify').onclick = function() {
|
||||
const textbox = document.getElementById('deleteModalReasonBox');
|
||||
if (this.checked && textbox.classList.contains('unfocused')) {
|
||||
textbox.classList.remove('unfocused');
|
||||
} else if (!this.checked) {
|
||||
textbox.classList.add('unfocused');
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('accountsTabDelete').onclick = function() {
|
||||
const deleteButton = this;
|
||||
let selected = [];
|
||||
const checkboxes = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]');
|
||||
for (check of checkboxes) {
|
||||
if (check.checked) {
|
||||
selected.push(check.id.replace('select_', ''));
|
||||
}
|
||||
}
|
||||
let title = " user";
|
||||
let msg = "Notify user";
|
||||
if (selected.length > 1) {
|
||||
title += "s";
|
||||
msg += "s";
|
||||
}
|
||||
title = "Delete " + selected.length + title;
|
||||
msg += " of account deletion";
|
||||
document.getElementById('deleteModalTitle').textContent = title;
|
||||
document.getElementById('deleteModalNotify').checked = false;
|
||||
document.getElementById('deleteModalNotifyLabel').textContent = msg;
|
||||
document.getElementById('deleteModalReason').value = '';
|
||||
document.getElementById('deleteModalReasonBox').classList.add('unfocused');
|
||||
document.getElementById('deleteModalSend').textContent = 'Delete';
|
||||
|
||||
document.getElementById('deleteModalSend').onclick = function() {
|
||||
const button = this;
|
||||
const send = {
|
||||
'users': selected,
|
||||
'notify': document.getElementById('deleteModalNotify').checked,
|
||||
'reason': document.getElementById('deleteModalReason').value
|
||||
};
|
||||
let req = new XMLHttpRequest();
|
||||
req.open("POST", "/deleteUser", true);
|
||||
req.responseType = 'json';
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
if (this.status == 500) {
|
||||
if ("error" in req.response) {
|
||||
button.textContent = 'Failed';
|
||||
} else {
|
||||
button.textContent = 'Partial fail (check console)';
|
||||
console.log(req.response);
|
||||
}
|
||||
setTimeout(function() {
|
||||
deleteModal.hide();
|
||||
deleteButton.classList.add('unfocused');
|
||||
}, 4000);
|
||||
} else {
|
||||
deleteButton.classList.add('unfocused');
|
||||
deleteModal.hide();
|
||||
}
|
||||
populateUsers();
|
||||
checkCheckboxes();
|
||||
}
|
||||
};
|
||||
req.send(JSON.stringify(send));
|
||||
};
|
||||
deleteModal.show();
|
||||
}
|
||||
|
||||
var jfUsers = [];
|
||||
|
||||
function validEmail(email) {
|
||||
const re = /\S+@\S+\.\S+/;
|
||||
return re.test(email);
|
||||
}
|
||||
|
||||
function changeEmail(icon, id) {
|
||||
const iconContent = icon.outerHTML;
|
||||
icon.setAttribute("class", "");
|
||||
const entry = icon.nextElementSibling;
|
||||
const ogEmail = entry.value;
|
||||
entry.readOnly = false;
|
||||
entry.classList.remove('form-control-plaintext');
|
||||
entry.classList.add('form-control');
|
||||
if (entry.value == "") {
|
||||
entry.placeholder = 'Address';
|
||||
}
|
||||
const tick = document.createElement('i');
|
||||
tick.classList.add("fa", "fa-check", "d-inline-block", "icon-button", "text-success");
|
||||
tick.setAttribute('style', 'margin-left: 0.5rem; margin-right: 0.5rem;');
|
||||
tick.onclick = function() {
|
||||
const newEmail = entry.value;
|
||||
if (!validEmail(newEmail) || newEmail == ogEmail) {
|
||||
return
|
||||
}
|
||||
cross.remove();
|
||||
this.outerHTML = `
|
||||
<div class="spinner-border spinner-border-sm" role="status" style="width: 1rem; height: 1rem; margin-left: 0.5rem;">
|
||||
<span class="sr-only">Saving...</span>
|
||||
</div>`;
|
||||
//this.remove();
|
||||
let send = {};
|
||||
send[id] = newEmail;
|
||||
let req = new XMLHttpRequest();
|
||||
req.open("POST", "/modifyEmails", true);
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
if (this.status == 200 || this.status == 204) {
|
||||
entry.nextElementSibling.remove();
|
||||
} else {
|
||||
entry.value = ogEmail;
|
||||
}
|
||||
}
|
||||
};
|
||||
req.send(JSON.stringify(send));
|
||||
icon.outerHTML = iconContent;
|
||||
entry.readOnly = true;
|
||||
entry.classList.remove('form-control');
|
||||
entry.classList.add('form-control-plaintext');
|
||||
entry.placeholder = '';
|
||||
};
|
||||
const cross = document.createElement('i');
|
||||
cross.classList.add("fa", "fa-close", "d-inline-block", "icon-button", "text-danger");
|
||||
cross.onclick = function() {
|
||||
tick.remove();
|
||||
this.remove();
|
||||
icon.outerHTML = iconContent;
|
||||
entry.readOnly = true;
|
||||
entry.classList.remove('form-control');
|
||||
entry.classList.add('form-control-plaintext');
|
||||
entry.placeholder = '';
|
||||
entry.value = ogEmail;
|
||||
};
|
||||
icon.parentNode.appendChild(tick);
|
||||
icon.parentNode.appendChild(cross);
|
||||
}
|
||||
|
||||
function populateUsers() {
|
||||
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>`;
|
||||
acList.parentNode.querySelector('thead').classList.add('unfocused');
|
||||
const accountsList = document.createElement('tbody');
|
||||
accountsList.id = 'accountsList';
|
||||
const generateEmail = function(id, name, email) {
|
||||
let entry = document.createElement('div');
|
||||
// entry.classList.add('py-1');
|
||||
entry.id = 'email_' + id;
|
||||
let emailValue = email;
|
||||
if (email === 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 = function(id, username, email, lastActive, admin) {
|
||||
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>
|
||||
`;
|
||||
};
|
||||
|
||||
let req = new XMLHttpRequest();
|
||||
req.responseType = 'json';
|
||||
req.open("GET", "/getUsers", true);
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
if (this.status == 200) {
|
||||
jfUsers = req.response['users'];
|
||||
for (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);
|
||||
}
|
||||
const header = acList.parentNode.querySelector('thead');
|
||||
if (header.classList.contains('unfocused')) {
|
||||
header.classList.remove('unfocused');
|
||||
}
|
||||
acList.replaceWith(accountsList);
|
||||
}
|
||||
}
|
||||
};
|
||||
req.send();
|
||||
}
|
||||
|
||||
document.getElementById('selectAll').checked = false;
|
||||
|
||||
document.getElementById('accountsTabSetDefaults').onclick = function() {
|
||||
const checkboxes = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]');
|
||||
let userIDs = [];
|
||||
for (check of checkboxes) {
|
||||
if (check.checked) {
|
||||
userIDs.push(check.id.replace('select_', ''));
|
||||
}
|
||||
}
|
||||
if (userIDs.length == 0) {
|
||||
return;
|
||||
}
|
||||
populateRadios();
|
||||
let userstring = 'user';
|
||||
if (userIDs.length > 1) {
|
||||
userstring += 's';
|
||||
}
|
||||
document.getElementById('defaultsTitle').textContent = `Apply settings to ${userIDs.length} ${userstring}`;
|
||||
document.getElementById('userDefaultsDescription').textContent = `
|
||||
Create an account and configure it to your liking, then choose it from below to apply to your selected users.`;
|
||||
document.getElementById('storeHomescreenLabel').textContent = `Apply homescreen layout`;
|
||||
if (document.getElementById('defaultsSourceSection').classList.contains('unfocused')) {
|
||||
document.getElementById('defaultsSourceSection').classList.remove('unfocused');
|
||||
}
|
||||
document.getElementById('defaultsSource').value = 'userTemplate';
|
||||
document.getElementById('defaultUserRadios').classList.add('unfocused');
|
||||
document.getElementById('storeDefaults').onclick = function() {
|
||||
storeDefaults(userIDs);
|
||||
};
|
||||
userDefaultsModal.show();
|
||||
};
|
||||
|
||||
document.getElementById('defaultsSource').addEventListener('change', function() {
|
||||
const radios = document.getElementById('defaultUserRadios');
|
||||
if (this.value == 'userTemplate') {
|
||||
radios.classList.add('unfocused');
|
||||
} else if (radios.classList.contains('unfocused')) {
|
||||
radios.classList.remove('unfocused');
|
||||
}
|
||||
})
|
||||
|
||||
document.getElementById('newUserCreate').onclick = function() {
|
||||
const ogText = this.textContent;
|
||||
this.innerHTML = `
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Creating...`;
|
||||
const email = document.getElementById('newUserEmail').value;
|
||||
var username = email;
|
||||
if (document.getElementById('newUserName') != null) {
|
||||
username = document.getElementById('newUserName').value;
|
||||
}
|
||||
const password = document.getElementById('newUserPassword').value;
|
||||
if (!validEmail(email) && email != "") {
|
||||
return;
|
||||
}
|
||||
const send = {
|
||||
'username': username,
|
||||
'password': password,
|
||||
'email': email
|
||||
}
|
||||
let req = new XMLHttpRequest()
|
||||
req.open("POST", "/newUserAdmin", true);
|
||||
req.responseType = 'json';
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
const button = this;
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
button.textContent = ogText;
|
||||
if (this.status == 200) {
|
||||
if (button.classList.contains('btn-primary')) {
|
||||
button.classList.remove('btn-primary');
|
||||
}
|
||||
button.classList.add('btn-success');
|
||||
button.textContent = 'Success';
|
||||
setTimeout(function() {
|
||||
if (button.classList.contains('btn-success')) {
|
||||
button.classList.remove('btn-success');
|
||||
}
|
||||
button.classList.add('btn-primary');
|
||||
button.textContent = ogText;
|
||||
newUserModal.hide();
|
||||
}, 1000);
|
||||
} else {
|
||||
if (button.classList.contains('btn-primary')) {
|
||||
button.classList.remove('btn-primary');
|
||||
}
|
||||
button.classList.add('btn-danger');
|
||||
if ("error" in req.response) {
|
||||
button.textContent = req.response["error"];
|
||||
} else {
|
||||
button.textContent = 'Failed';
|
||||
}
|
||||
setTimeout(function() {
|
||||
if (button.classList.contains('btn-danger')) {
|
||||
button.classList.remove('btn-danger');
|
||||
}
|
||||
button.classList.add('btn-primary');
|
||||
button.textContent = ogText;
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
};
|
||||
req.send(JSON.stringify(send));
|
||||
}
|
||||
|
||||
document.getElementById('accountsTabAddUser').onclick = function() {
|
||||
document.getElementById('newUserEmail').value = '';
|
||||
document.getElementById('newUserPassword').value = '';
|
||||
if (document.getElementById('newUserName') != null) {
|
||||
document.getElementById('newUserName').value = '';
|
||||
}
|
||||
newUserModal.show();
|
||||
};
|
||||
1133
data/static/admin.js
1133
data/static/admin.js
File diff suppressed because it is too large
Load Diff
@@ -1,32 +0,0 @@
|
||||
function serializeForm(id) {
|
||||
var form = document.getElementById(id);
|
||||
var formData = {};
|
||||
for (var i = 0; i < form.elements.length; i++) {
|
||||
var el = form.elements[i];
|
||||
if (el.type != 'submit') {
|
||||
var name = el.name;
|
||||
if (name == '') {
|
||||
name = el.id;
|
||||
};
|
||||
switch (el.type) {
|
||||
case 'checkbox':
|
||||
formData[name] = el.checked;
|
||||
break;
|
||||
case 'text':
|
||||
case 'password':
|
||||
case 'email':
|
||||
case 'number':
|
||||
formData[name] = el.value;
|
||||
break;
|
||||
case 'select-one':
|
||||
let val = el.value;
|
||||
if (!isNaN(val)) {
|
||||
val = parseInt(val)
|
||||
}
|
||||
formData[name] = val;
|
||||
break;
|
||||
};
|
||||
};
|
||||
};
|
||||
return formData;
|
||||
};
|
||||
@@ -133,7 +133,7 @@ document.getElementById('jfTestButton').onclick = function() {
|
||||
nextButton.classList.add('disabled');
|
||||
nextButton.setAttribute('aria-disabled', 'true');
|
||||
var req = new XMLHttpRequest();
|
||||
req.open("POST", "/testJF", true);
|
||||
req.open("POST", "/jellyfin/test", true);
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.responseType = 'json';
|
||||
req.onreadystatechange = function() {
|
||||
@@ -260,7 +260,7 @@ document.getElementById('submitButton').onclick = function() {
|
||||
// Send it
|
||||
config["restart-program"] = true;
|
||||
var req = new XMLHttpRequest();
|
||||
req.open("POST", "/modifyConfig", true);
|
||||
req.open("POST", "/config", true);
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.responseType = 'json';
|
||||
req.onreadystatechange = function() {
|
||||
|
||||
@@ -31,11 +31,11 @@
|
||||
return "";
|
||||
}
|
||||
{{ if .bs5 }}
|
||||
const bsVersion = 5;
|
||||
window.bsVersion = 5;
|
||||
{{ else }}
|
||||
const bsVersion = 4;
|
||||
window.bsVersion = 4;
|
||||
{{ end }}
|
||||
var cssFile = "{{ .cssFile }}";
|
||||
window.cssFile = "{{ .cssFile }}";
|
||||
var css = document.createElement('link');
|
||||
css.setAttribute('rel', 'stylesheet');
|
||||
css.setAttribute('type', 'text/css');
|
||||
@@ -52,8 +52,6 @@
|
||||
}
|
||||
css.setAttribute('href', cssFile);
|
||||
document.head.appendChild(css);
|
||||
// store whether ombi is enabled, 1 or 0.
|
||||
var ombiEnabled = {{ .ombiEnabled }}
|
||||
</script>
|
||||
{{ if not .bs5 }}
|
||||
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
|
||||
@@ -90,47 +88,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="settingsMenu" role="dialog" aria-labelledby="settings menu" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="settingsTitle">Settings</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ul class="list-group list-group-flush" style="margin-bottom: 1rem;">
|
||||
<p>Note: <sup class="text-danger">*</sup> Indicates required field, <sup class="text-danger">R</sup> Indicates changes require a restart.</p>
|
||||
<button type="button" class="list-group-item list-group-item-action" id="openAbout">
|
||||
About <i class="fa fa-info-circle settingIcon"></i>
|
||||
</button>
|
||||
<button type="button" class="list-group-item list-group-item-action" id="openDefaultsWizard">
|
||||
New User Defaults <i class="fa fa-user settingIcon"></i>
|
||||
</button>
|
||||
{{ if .ombiEnabled }}
|
||||
<button type="button" class="list-group-item list-group-item-action" id="openOmbiDefaults">
|
||||
Ombi User Defaults <i class="fa fa-chain-broken settingIcon"></i>
|
||||
</button>
|
||||
{{ end }}
|
||||
</ul>
|
||||
<div class="list-group list-group-flush" id="settingsList">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer" id="settingsFooter">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" id="settingsSave">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="users" role="dialog" aria-labelledby="users" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="usersTitle">Users</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<button type="button" class="{{ if .bs5 }}btn-{{ end }}close" data-dismiss="modal" aria-label="Close">
|
||||
{{ if not .bs5 }}
|
||||
<span aria-hidden="true">×</span>
|
||||
{{ end }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@@ -148,20 +114,34 @@
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="defaultsTitle"></h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<button type="button" class="{{ if .bs5 }}btn-{{ end }}close" data-dismiss="modal" aria-label="Close">
|
||||
{{ if not .bs5 }}
|
||||
<span aria-hidden="true">×</span>
|
||||
{{ end }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="userDefaultsDescription"></p>
|
||||
<div class="mb-3" id="defaultsSourceSection">
|
||||
<label for="defaultsSource">Use settings from:</label>
|
||||
<label for="defaultsSource" class="form-label">Use settings from:</label>
|
||||
<select class="form-select" id="defaultsSource" aria-label="User settings source">
|
||||
<option value="userTemplate" selected>Use existing user template</option>
|
||||
<option value="profile" selected>Profile</option>
|
||||
<option value="fromUser">Source from existing user</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="defaultUserRadios"></div>
|
||||
<div class="mb-3 unfocused" id="profileSelectBox">
|
||||
<label for="profileSelect" class="form-label">Profile</label>
|
||||
<select class="form-select" id="profileSelect" aria-label="Profile to apply">
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3 unfocused" id="newProfileBox">
|
||||
<label for="newProfileName" class="form-label">Name</label>
|
||||
<input type="text" class="form-control" id="newProfileName" aria-describedby="Profile Name">
|
||||
</div>
|
||||
<div id="defaultUserRadiosBox">
|
||||
<label for="defaultUserRadios" class="form-label">Get settings from</label>
|
||||
<div id="defaultUserRadios"></div>
|
||||
</div>
|
||||
<div class="form-check" style="margin-top: 1rem;">
|
||||
<input class="form-check-input" type="checkbox" value="" id="storeDefaultHomescreen" checked>
|
||||
<label class="form-check-label" for="storeDefaultHomescreen" id="storeHomescreenLabel"></label>
|
||||
@@ -180,8 +160,10 @@
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="ombiTitle">Ombi user defaults</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<button type="button" class="{{ if .bs5 }}btn-{{ end }}close" data-dismiss="modal" aria-label="Close">
|
||||
{{ if not .bs5 }}
|
||||
<span aria-hidden="true">×</span>
|
||||
{{ end }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@@ -206,7 +188,7 @@
|
||||
<p>A restart is needed to apply some settings. Restart now, later, or cancel?</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-light" data-dismiss="modal" id="restartModalCancel">Cancel</button>
|
||||
<button type="button" class="btn btn-secondary" id="applyRestarts" data-dismiss="modal">Apply, Restart later</button>
|
||||
<button type="button" class="btn btn-primary" id="applyAndRestart" data-dismiss="modal">Apply & Restart</button>
|
||||
</div>
|
||||
@@ -230,15 +212,17 @@
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">About</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<button type="button" class="{{ if .bs5 }}btn-{{ end }}close" data-dismiss="modal" aria-label="Close">
|
||||
{{ if not .bs5 }}
|
||||
<span aria-hidden="true">×</span>
|
||||
{{ end }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<img src="banner.svg" alt="jfa-go banner">
|
||||
<p><a href="https://github.com/hrfee/jfa-go"><i class="fa fa-github"></i> jfa-go</a></p>
|
||||
<p>Version <i>{{ .version }}</i></p>
|
||||
<p>Commit <i>{{ .commit }}</i></p>
|
||||
<p>Version <i class="text-monospace">{{ .version }}</i></p>
|
||||
<p>Commit <i class="text-monospace">{{ .commit }}</i></p>
|
||||
<p><a href="https://github.com/hrfee/jfa-go/blob/main/LICENSE">Available under the MIT License.</a></p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -249,8 +233,10 @@
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteModalTitle"></h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<button type="button" class="{{ if .bs5 }}btn-{{ end }}close" data-dismiss="modal" aria-label="Close">
|
||||
{{ if not .bs5 }}
|
||||
<span aria-hidden="true">×</span>
|
||||
{{ end }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@@ -275,8 +261,10 @@
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Create a user</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<button type="button" class="{{ if .bs5 }}btn-{{ end }}close" data-dismiss="modal" aria-label="Close">
|
||||
{{ if not .bs5 }}
|
||||
<span aria-hidden="true">×</span>
|
||||
{{ end }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@@ -310,12 +298,12 @@
|
||||
<li class="nav-item">
|
||||
<h2><a id="accountsTabButton" class="nl nav-link">Accounts</a></h2>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<h2><a id="settingsTabButton" class="nl nav-link">Settings</a></h2>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="btn-group" role="group" id="headerButtons">
|
||||
<button type="button" class="btn btn-primary" id="openSettings">
|
||||
Settings <i class="fa fa-cog"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger" id="logoutButton" style="display: none;">
|
||||
<button type="button" class="btn btn-danger unfocused" id="logoutButton">
|
||||
Logout <i class="fa fa-sign-out"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -367,6 +355,11 @@
|
||||
</label>
|
||||
<div id="noLimitWarning" class="form-text" style="display: none;">Warning: Unlimited usage invites pose a risk if published online.</div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 1rem;">
|
||||
<label for="inviteProfile">Account creation profile</label>
|
||||
<select class="form-control form-select" id="inviteProfile" name="profile">
|
||||
</select>
|
||||
</div>
|
||||
{{ if .email_enabled }}
|
||||
<div class="form-group">
|
||||
<label for="send_to_address">Send invite to address</label>
|
||||
@@ -416,7 +409,6 @@
|
||||
<th scope="col">Username</th>
|
||||
<th scope="col">Email Address</th>
|
||||
<th scope="col">Last Active</th>
|
||||
<th scope="col">Admin?</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="accountsList">
|
||||
@@ -425,86 +417,78 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="settingsTab" class="unfocused mb-3 tabGroup card">
|
||||
<div class="card-header d-flex" style="align-items: center;">
|
||||
<div>Settings</div>
|
||||
<div class="ml-auto">
|
||||
<button type="button" class="btn btn-primary" id="settingsSave">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container card-body">
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="" id="settingsLeft">
|
||||
<ul class="list-group list-group-flush" style="margin-bottom: 1rem;">
|
||||
<p>Note: <sup class="text-danger">*</sup> Indicates required field, <sup class="text-danger">R</sup> Indicates changes require a restart.</p>
|
||||
<button type="button" class="list-group-item list-group-item-action static" id="openAbout">
|
||||
About <i class="fa fa-info-circle settingIcon"></i>
|
||||
</button>
|
||||
<button type="button" class="list-group-item list-group-item-action" id="profiles_button">
|
||||
User Profiles <i class="fa fa-user settingIcon"></i>
|
||||
</button>
|
||||
{{ if .ombiEnabled }}
|
||||
<button type="button" class="list-group-item list-group-item-action static" id="openOmbiDefaults" onclick="window.openOmbiDefaults()">
|
||||
Ombi User Defaults <i class="fa fa-chain-broken settingIcon"></i>
|
||||
</button>
|
||||
{{ end }}
|
||||
</ul>
|
||||
<div class="list-group list-group-flush" id="settingsSections">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="" id="settingsContent">
|
||||
<div id="profiles" class="unfocused">
|
||||
<div class="card card-body">
|
||||
<p>Profiles are applied to users when they create an account. They include things like access rights and homescreen layout. You can create them here.</p>
|
||||
<table class="table table-sm table-striped table-borderless">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Default</th>
|
||||
<th scope="col">From</th>
|
||||
<th scope="col">Admin?</th>
|
||||
<th scope="col">Libraries</th>
|
||||
<th scope="col"><button class="btn btn-outline-primary" onclick="createProfile()">Create</button></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="profileList">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="contactBox">
|
||||
<p>{{ .contactMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<script src="serialize.js"></script>
|
||||
<script>
|
||||
{{ if .bs5 }}
|
||||
function createModal(id, find = false) {
|
||||
let modal;
|
||||
if (find) {
|
||||
modal = bootstrap.Modal.getInstance(document.getElementById(id));
|
||||
} else {
|
||||
modal = new bootstrap.Modal(document.getElementById(id));
|
||||
}
|
||||
document.getElementById(id).addEventListener('shown.bs.modal', function () {
|
||||
document.body.classList.add("modal-open");
|
||||
});
|
||||
return {
|
||||
modal: modal,
|
||||
show: function() {
|
||||
let temp = this.modal.show();
|
||||
return temp
|
||||
},
|
||||
hide: function() { return this.modal.hide(); }
|
||||
};
|
||||
}
|
||||
{{ else }}
|
||||
let send_to_addess_enabled = document.getElementById('send_to_address_enabled');
|
||||
if (typeof(send_to_address_enabled) != 'undefined') {
|
||||
send_to_address_enabled.classList.remove('form-check-input');
|
||||
}
|
||||
let multiUseEnabled = document.getElementById('multiUseEnabled');
|
||||
if (typeof(multiUseEnabled) != 'undefined') {
|
||||
multiUseEnabled.classList.remove('form-check-input');
|
||||
}
|
||||
function createModal(id, find = false) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
document.body.classList.add("modal-open");
|
||||
});
|
||||
return {
|
||||
show: function() {
|
||||
let temp = $('#' + id).modal('show');
|
||||
return temp
|
||||
},
|
||||
hide: function() {
|
||||
return $('#' + id).modal('hide');
|
||||
}
|
||||
};
|
||||
}
|
||||
{{ end }}
|
||||
|
||||
function triggerTooltips() {
|
||||
{{ if .bs5 }}
|
||||
document.getElementById('settingsMenu').addEventListener('shown.bs.modal', function() {
|
||||
{{ else }}
|
||||
$('#settingsMenu').on('shown.bs.modal', function() {
|
||||
{{ end }}
|
||||
// Hacky way to ensure anything dependent on checkbox state is disabled if necessary by just clicking them
|
||||
let checkboxes = document.getElementById('settingsMenu').querySelectorAll('input[type="checkbox"]');
|
||||
for (checkbox of checkboxes) {
|
||||
checkbox.click();
|
||||
checkbox.click();
|
||||
}
|
||||
let tooltips = [].slice.call(document.querySelectorAll('a[data-toggle="tooltip"]'));
|
||||
tooltips.map(function(el) {
|
||||
{{ if .bs5 }}
|
||||
return new bootstrap.Tooltip(el);
|
||||
{{ else }}
|
||||
return $(el).tooltip();
|
||||
{{ end }}
|
||||
});
|
||||
});
|
||||
}
|
||||
window.bs5 = {{ .bs5 }};
|
||||
window.availableProfiles = [];
|
||||
{{ if .notifications }}
|
||||
const notifications_enabled = true;
|
||||
window.notifications_enabled = true;
|
||||
{{ else }}
|
||||
const notifications_enabled = false;
|
||||
window.notifications_enabled = false;
|
||||
{{ end }}
|
||||
</script>
|
||||
<script src="accounts.js"></script>
|
||||
<script src="admin.js"></script>
|
||||
<script src="admin.js" type="module"></script>
|
||||
<script src="invites.js" type="module"></script>
|
||||
{{ if .ombiEnabled }}
|
||||
<script src="ombi.js" type="module"></script>
|
||||
{{ end }}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
9
data/templates/form-base.html
Normal file
9
data/templates/form-base.html
Normal file
@@ -0,0 +1,9 @@
|
||||
{{ define "form-base" }}
|
||||
<script>
|
||||
window.bs5 = {{ .settings.bs5 }};
|
||||
window.usernameEnabled = {{ .settings.username }};
|
||||
window.validationStrings = JSON.parse({{ .lang.validationStrings }});
|
||||
window.invalidPassword = "{{ .lang.reEnterPasswordInvalid }}";
|
||||
</script>
|
||||
<script src="form.js" type="module"></script>
|
||||
{{ end }}
|
||||
1
data/templates/form-loader.html
Normal file
1
data/templates/form-loader.html
Normal file
@@ -0,0 +1 @@
|
||||
{{ template "form.html" . }}
|
||||
@@ -14,11 +14,11 @@
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<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>
|
||||
{{ 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>
|
||||
{{ 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>
|
||||
{{ else }}
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
|
||||
@@ -41,27 +41,27 @@
|
||||
margin-bottom: 5%;
|
||||
}
|
||||
</style>
|
||||
<title>Create Jellyfin Account</title>
|
||||
<title>{{ .lang.pageTitle }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="modal fade" id="successBox" tabindex="-1" role="dialog" aria-labelledby="successBox" aria-hidden="true" data-backdrop="static">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="successTitle">Success!</h5>
|
||||
<h5 class="modal-title" id="successHeader">{{ .lang.successHeader }}</h5>
|
||||
</div>
|
||||
<div class="modal-body" id="successBody">
|
||||
<p>{{ .successMessage }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="{{ .jfLink }}" class="btn btn-primary">Continue</a>
|
||||
<a href="{{ .jfLink }}" class="btn btn-primary">{{ .lang.successContinueButton }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pageContainer">
|
||||
<h1>
|
||||
Create Account
|
||||
{{ .lang.createAccountHeader }}
|
||||
</h1>
|
||||
<p>{{ .helpMessage }}</p>
|
||||
<p class="contactBox">{{ .contactMessage }}</p>
|
||||
@@ -69,26 +69,30 @@
|
||||
<div class="row" id="cardContainer">
|
||||
<div class="col-sm">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">Details</div>
|
||||
<div class="card-header">{{ .lang.accountDetails }}</div>
|
||||
<div class="card-body">
|
||||
<form action="#" method="POST" id="accountForm">
|
||||
<div class="form-group">
|
||||
<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>
|
||||
<label for="inputEmail">{{ .lang.emailAddress }}</label>
|
||||
<input type="email" class="form-control" id="{{ if .settings.username }}inputEmail{{ else }}inputUsername{{ end }}" name="{{ if .settings.username }}email{{ else }}username{{ end }}" placeholder="{{ .lang.emailAddress }}" value="{{ .email }}" required>
|
||||
</div>
|
||||
{{ if .username }}
|
||||
{{ if .settings.username }}
|
||||
<div class="form-group">
|
||||
<label for="inputUsername">Username</label>
|
||||
<input type="username" class="form-control" id="inputUsername" name="username" placeholder="Username" required>
|
||||
<label for="inputUsername">{{ .lang.username }}</label>
|
||||
<input type="username" class="form-control" id="inputUsername" name="username" placeholder="{{ .lang.username }}" required>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div class="form-group">
|
||||
<label for="inputPassword">Password</label>
|
||||
<input type="password" class="form-control" id="inputPassword" name="password" placeholder="Password" required>
|
||||
<label for="inputPassword">{{ .lang.password }}</label>
|
||||
<input type="password" class="form-control" id="inputPassword" name="password" placeholder="{{ .lang.password }}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="inputPassword">{{ .lang.reEnterPassword }}</label>
|
||||
<input type="password" class="form-control" id="reInputPassword" onkeyup="window.checkPassword()" placeholder="{{ .lang.password }}" required>
|
||||
</div>
|
||||
<div class="btn-group" role="group" aria-label="Button & Error" id="errorBox" style="margin-top: 1rem;">
|
||||
<button type="submit" class="btn btn-outline-primary" id="submitButton">
|
||||
<span id="createAccount">Create Account</span>
|
||||
<span id="createAccount">{{ .lang.createAccountButton }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -98,12 +102,12 @@
|
||||
{{ if .validate }}
|
||||
<div class="col-sm" id="requirementBox">
|
||||
<div class="card mb-3 requirementBox">
|
||||
<div class="card-header">Password Requirements</div>
|
||||
<div class="card-header">{{ .lang.passwordRequirementsHeader }}</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-group">
|
||||
{{ range $key, $value := .requirements }}
|
||||
<li id="{{ $key }}" class="list-group-item list-group-item-danger">
|
||||
<div> {{ $value }}</div>
|
||||
<li id="{{ $key }}" min="{{ $value }}" class="list-group-item list-group-item-danger">
|
||||
<div></div>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
@@ -114,109 +118,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="serialize.js"></script>
|
||||
<script>
|
||||
{{ if .bs5 }}
|
||||
var bsVersion = 5;
|
||||
{{ else }}
|
||||
var bsVersion = 4;
|
||||
{{ end }}
|
||||
if (bsVersion == 5) {
|
||||
var successBox = new bootstrap.Modal(document.getElementById('successBox'));
|
||||
} else if (bsVersion == 4) {
|
||||
var successBox = {
|
||||
show : function() {
|
||||
return $('#successBox').modal('show');
|
||||
},
|
||||
hide : function() {
|
||||
return $('#successBox').modal('hide');
|
||||
}
|
||||
};
|
||||
};
|
||||
var code = window.location.href.split('/').pop();
|
||||
function toggleSpinner () {
|
||||
var submitButton = document.getElementById('submitButton');
|
||||
var oldSpan = document.getElementById('createAccount');
|
||||
var newSpan = document.createElement('span');
|
||||
newSpan.id = 'createAccount';
|
||||
if (document.getElementById('createAccountSpinner')) {
|
||||
newSpan.appendChild(document.createTextNode('Create Account'));
|
||||
submitButton.disabled = false;
|
||||
} else {
|
||||
var spinner = document.createElement('span');
|
||||
spinner.id = 'createAccountSpinner';
|
||||
spinner.classList.add('spinner-border', 'spinner-border-sm');
|
||||
spinner.setAttribute('role', 'status');
|
||||
spinner.setAttribute('aria-hidden', 'true');
|
||||
var text = document.createTextNode(' Creating...');
|
||||
newSpan.appendChild(spinner);
|
||||
newSpan.appendChild(text);
|
||||
submitButton.disabled = true;
|
||||
}
|
||||
submitButton.replaceChild(newSpan, oldSpan);
|
||||
};
|
||||
document.getElementById('accountForm').onsubmit = function() {
|
||||
if (document.getElementById('errorMessage')) {
|
||||
document.getElementById('errorMessage').remove();
|
||||
}
|
||||
toggleSpinner();
|
||||
var send = serializeForm('accountForm');
|
||||
send['code'] = code;
|
||||
{{ if not .username }}
|
||||
send['email'] = send['username'];
|
||||
{{ end }}
|
||||
send = JSON.stringify(send);
|
||||
var req = new XMLHttpRequest();
|
||||
req.open("POST", "/newUser", true);
|
||||
req.responseType = 'json';
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
toggleSpinner();
|
||||
var data = this.response;
|
||||
if ('error' in data || data['success'] == false) {
|
||||
if (typeof(data['error']) != 'undefined') {
|
||||
var errorMessage = data['error'];
|
||||
} else {
|
||||
var errorMessage = 'Unknown Error';
|
||||
}
|
||||
var text = document.createTextNode(errorMessage);
|
||||
var error = document.createElement('button');
|
||||
error.classList.add('btn', 'btn-outline-danger');
|
||||
error.setAttribute('disabled', '');
|
||||
error.appendChild(text);
|
||||
error.id = 'errorMessage';
|
||||
document.getElementById('errorBox').appendChild(error);
|
||||
} else {
|
||||
var valid = true
|
||||
for (var key in data) {
|
||||
if (data.hasOwnProperty(key)) {
|
||||
var criterion = document.getElementById(key);
|
||||
if (criterion) {
|
||||
if (data[key] == false) {
|
||||
valid = false;
|
||||
if (criterion.classList.contains('list-group-item-success')) {
|
||||
criterion.classList.remove('list-group-item-success');
|
||||
criterion.classList.add('list-group-item-danger');
|
||||
};
|
||||
} else {
|
||||
if (criterion.classList.contains('list-group-item-danger')) {
|
||||
criterion.classList.remove('list-group-item-danger');
|
||||
criterion.classList.add('list-group-item-success');
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
if (valid == true) {
|
||||
successBox.show();
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
req.send(send);
|
||||
return false;
|
||||
};
|
||||
window.validationStrings = {{ .lang.validationStrings }};
|
||||
</script>
|
||||
{{ template "form-base" . }}
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
|
||||
8
docs/go.mod
Normal file
8
docs/go.mod
Normal file
@@ -0,0 +1,8 @@
|
||||
module github.com/hrfee/jfa-go/docs
|
||||
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
|
||||
github.com/swaggo/swag v1.6.7
|
||||
)
|
||||
27
email.go
27
email.go
@@ -41,10 +41,10 @@ func (mg *Mailgun) send(address, fromName, fromAddr string, email *Email) error
|
||||
|
||||
// SMTP supports SSL/TLS and STARTTLS; implements emailClient.
|
||||
type SMTP struct {
|
||||
sslTLS bool
|
||||
host, server string
|
||||
port int
|
||||
auth smtp.Auth
|
||||
sslTLS bool
|
||||
server string
|
||||
port int
|
||||
auth smtp.Auth
|
||||
}
|
||||
|
||||
func (sm *SMTP) send(address, fromName, fromAddr string, email *Email) error {
|
||||
@@ -54,12 +54,14 @@ func (sm *SMTP) send(address, fromName, fromAddr string, email *Email) error {
|
||||
e.To = []string{address}
|
||||
e.Text = []byte(email.text)
|
||||
e.HTML = []byte(email.html)
|
||||
server := fmt.Sprintf("%s:%d", sm.server, sm.port)
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: false,
|
||||
ServerName: sm.host,
|
||||
ServerName: sm.server,
|
||||
}
|
||||
server := fmt.Sprintf("%s:%d", sm.server, sm.port)
|
||||
var err error
|
||||
fmt.Println(server)
|
||||
// err = e.Send(server, sm.auth)
|
||||
if sm.sslTLS {
|
||||
err = e.SendWithTLS(server, sm.auth, tlsConfig)
|
||||
} else {
|
||||
@@ -113,7 +115,13 @@ func NewEmailer(app *appContext) *Emailer {
|
||||
if app.config.Section("smtp").Key("encryption").String() == "ssl_tls" {
|
||||
sslTls = true
|
||||
}
|
||||
emailer.NewSMTP(app.config.Section("smtp").Key("server").String(), app.config.Section("smtp").Key("port").MustInt(465), app.config.Section("smtp").Key("password").String(), app.host, sslTls)
|
||||
username := ""
|
||||
if u := app.config.Section("smtp").Key("username").MustString(""); u != "" {
|
||||
username = u
|
||||
} else {
|
||||
username = emailer.fromAddr
|
||||
}
|
||||
emailer.NewSMTP(app.config.Section("smtp").Key("server").String(), app.config.Section("smtp").Key("port").MustInt(465), username, app.config.Section("smtp").Key("password").String(), sslTls)
|
||||
} else if method == "mailgun" {
|
||||
emailer.NewMailgun(app.config.Section("mailgun").Key("api_url").String(), app.config.Section("mailgun").Key("api_key").String())
|
||||
}
|
||||
@@ -135,11 +143,10 @@ func (emailer *Emailer) NewMailgun(url, key string) {
|
||||
}
|
||||
|
||||
// NewSMTP returns an SMTP emailClient.
|
||||
func (emailer *Emailer) NewSMTP(server string, port int, password, host string, sslTLS bool) {
|
||||
func (emailer *Emailer) NewSMTP(server string, port int, username, password string, sslTLS bool) {
|
||||
emailer.sender = &SMTP{
|
||||
auth: smtp.PlainAuth("", emailer.fromAddr, password, host),
|
||||
auth: smtp.PlainAuth("", username, password, server),
|
||||
server: server,
|
||||
host: host,
|
||||
port: port,
|
||||
sslTLS: sslTLS,
|
||||
}
|
||||
|
||||
5
esbuild.sh
Executable file
5
esbuild.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/bash
|
||||
# set +e
|
||||
# npx tsc -p ts/
|
||||
# set -e
|
||||
npx esbuild ts/* --outdir=data/static --minify
|
||||
46
go.mod
46
go.mod
@@ -2,26 +2,54 @@ module github.com/hrfee/jfa-go
|
||||
|
||||
go 1.14
|
||||
|
||||
replace github.com/hrfee/jfa-go/docs => ./docs
|
||||
|
||||
replace github.com/hrfee/jfa-go/jfapi => ./jfapi
|
||||
|
||||
replace github.com/hrfee/jfa-go/common => ./common
|
||||
|
||||
replace github.com/hrfee/jfa-go/ombi => ./ombi
|
||||
|
||||
require (
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/fsnotify/fsnotify v1.4.9
|
||||
github.com/gin-contrib/pprof v1.3.0
|
||||
github.com/gin-contrib/static v0.0.0-20200815103939-31fb0c56a3d1
|
||||
github.com/gin-contrib/static v0.0.0-20200916080430-d45d9a37d28e
|
||||
github.com/gin-gonic/gin v1.6.3
|
||||
github.com/go-chi/chi v4.1.2+incompatible // indirect
|
||||
github.com/go-playground/validator/v10 v10.3.0 // indirect
|
||||
github.com/golang/protobuf v1.4.2 // indirect
|
||||
github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/spec v0.19.13 // indirect
|
||||
github.com/go-playground/validator/v10 v10.4.1 // indirect
|
||||
github.com/golang/protobuf v1.4.3
|
||||
github.com/google/uuid v1.1.2 // indirect
|
||||
github.com/hrfee/jfa-go/common v0.0.0-20201112212552-b6f3cd7c1f71
|
||||
github.com/hrfee/jfa-go/docs v0.0.0-20201112212552-b6f3cd7c1f71
|
||||
github.com/hrfee/jfa-go/jfapi v0.0.0-20201112212552-b6f3cd7c1f71
|
||||
github.com/hrfee/jfa-go/ombi v0.0.0-20201112212552-b6f3cd7c1f71
|
||||
github.com/jordan-wright/email v4.0.1-0.20200917010138-e1c00e156980+incompatible
|
||||
github.com/json-iterator/go v1.1.10 // indirect
|
||||
github.com/knz/strtime v0.0.0-20200318182718-be999391ffa9
|
||||
github.com/knz/strtime v0.0.0-20200924090105-187c67f2bf5e
|
||||
github.com/lithammer/shortuuid/v3 v3.0.4
|
||||
github.com/mailgun/mailgun-go/v4 v4.1.3
|
||||
github.com/mailru/easyjson v0.7.3 // indirect
|
||||
github.com/logrusorgru/aurora/v3 v3.0.0
|
||||
github.com/mailgun/mailgun-go/v4 v4.3.0
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||
github.com/pdrum/swagger-automation v0.0.0-20190629163613-c8c7c80ba858
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14
|
||||
github.com/swaggo/gin-swagger v1.3.0
|
||||
github.com/swaggo/swag v1.6.9 // indirect
|
||||
github.com/ugorji/go v1.2.0 // indirect
|
||||
github.com/urfave/cli/v2 v2.3.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9 // indirect
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b // indirect
|
||||
golang.org/x/sys v0.0.0-20201113233024-12cec1faf1ba // indirect
|
||||
golang.org/x/text v0.3.4 // indirect
|
||||
golang.org/x/tools v0.0.0-20201114224030-61ea331ec02b // indirect
|
||||
google.golang.org/protobuf v1.25.0 // indirect
|
||||
gopkg.in/ini.v1 v1.60.0
|
||||
gopkg.in/ini.v1 v1.62.0
|
||||
gopkg.in/yaml.v2 v2.3.0 // indirect
|
||||
)
|
||||
|
||||
243
go.sum
243
go.sum
@@ -1,7 +1,20 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v1.0.2 h1:KPldsxuKGsS2FPWsNeg9ZO18aCrGKujPoWXn2yo+KQM=
|
||||
@@ -15,14 +28,23 @@ github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqL
|
||||
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w=
|
||||
github.com/gin-contrib/pprof v1.3.0 h1:G9eK6HnbkSqDZBYbzG4wrjCsA4e+cvYAHUZw6W+W9K0=
|
||||
github.com/gin-contrib/pprof v1.3.0/go.mod h1:waMjT1H9b179t3CxuG1cV3DHpga6ybizwfBaM5OXaB0=
|
||||
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
||||
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2 h1:xLG16iua01X7Gzms9045s2Y2niNpvSY/Zb1oBwgNYZY=
|
||||
github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2/go.mod h1:VhW/Ch/3FhimwZb8Oj+qJmdMmoB8r7lmJ5auRjm50oQ=
|
||||
github.com/gin-contrib/static v0.0.0-20200815103939-31fb0c56a3d1 h1:plQYoJeO9lI8Ag0xZy7dDF8FMwIOHsQylKjcclknvIc=
|
||||
github.com/gin-contrib/static v0.0.0-20200815103939-31fb0c56a3d1/go.mod h1:VhW/Ch/3FhimwZb8Oj+qJmdMmoB8r7lmJ5auRjm50oQ=
|
||||
github.com/gin-contrib/static v0.0.0-20200916080430-d45d9a37d28e h1:8bZpGwoPxkaivQPrAbWl+7zjjUcbFUnYp7yQcx2r2N0=
|
||||
github.com/gin-contrib/static v0.0.0-20200916080430-d45d9a37d28e/go.mod h1:VhW/Ch/3FhimwZb8Oj+qJmdMmoB8r7lmJ5auRjm50oQ=
|
||||
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
|
||||
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
|
||||
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
|
||||
github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||
@@ -31,6 +53,40 @@ github.com/go-chi/chi v4.0.0+incompatible h1:SiLLEDyAkqNnw+T/uDTf3aFB9T4FTrwMpuY
|
||||
github.com/go-chi/chi v4.0.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
|
||||
github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
|
||||
github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
|
||||
github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
|
||||
github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
|
||||
github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
|
||||
github.com/go-openapi/jsonreference v0.19.3 h1:5cxNfTy0UVC3X8JL5ymxzyoUZmo8iZb+jeTWn7tUa8o=
|
||||
github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
|
||||
github.com/go-openapi/jsonreference v0.19.4 h1:3Vw+rh13uq2JFNxgnMTGE1rnoieU9FmyE1gvnyylsYg=
|
||||
github.com/go-openapi/jsonreference v0.19.4/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
|
||||
github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
|
||||
github.com/go-openapi/spec v0.19.4 h1:ixzUSnHTd6hCemgtAJgluaTSGYpLNpJY4mA2DIkdOAo=
|
||||
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/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/spec v0.19.12 h1:OO9WrvhDwtiMY/Opr1j1iFZzirI3JW4/bxNFRcntAr4=
|
||||
github.com/go-openapi/spec v0.19.12/go.mod h1:gwrgJS15eCUgjLpMjBJmbZezCsw88LmgeEip0M63doA=
|
||||
github.com/go-openapi/spec v0.19.13 h1:AcZVcWsrfW7LqyHKVbTZYpFF7jQcMxmAsWrw2p/b9ew=
|
||||
github.com/go-openapi/spec v0.19.13/go.mod h1:gwrgJS15eCUgjLpMjBJmbZezCsw88LmgeEip0M63doA=
|
||||
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.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY=
|
||||
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/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-openapi/swag v0.19.11 h1:RFTu/dlFySpyVvJDfp/7674JY4SDglYWKztbiIGFpmc=
|
||||
github.com/go-openapi/swag v0.19.11/go.mod h1:Uc0gKkdR+ojzsEpjh39QChyu92vPgIr72POcgHMAgSY=
|
||||
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.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
@@ -42,9 +98,15 @@ github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/go-playground/validator/v10 v10.3.0 h1:nZU+7q+yJoFmwvNgv/LnPUkwPal62+b2xXj0AU1Es7o=
|
||||
github.com/go-playground/validator/v10 v10.3.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/go-playground/validator/v10 v10.4.0 h1:72qIR/m8ybvL8L5TIyfgrigqkrw7kVYAvjEvpT85l70=
|
||||
github.com/go-playground/validator/v10 v10.4.0/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
@@ -56,6 +118,8 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
@@ -64,10 +128,16 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e h1:OGunVjqY7y4U4laftpEHv+mvZBlr7UGimJXKEGQtg48=
|
||||
github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e/go.mod h1:Fy2gCFfZhay8jplf/Csj6cyH/oshQTkLQYZbKkcV+SY=
|
||||
github.com/jordan-wright/email v4.0.1-0.20200917010138-e1c00e156980+incompatible h1:CL0ooBNfbNyJTJATno+m0h+zM5bW6v7fKlboKUGP/dI=
|
||||
github.com/jordan-wright/email v4.0.1-0.20200917010138-e1c00e156980+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
@@ -75,6 +145,16 @@ github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/knz/strtime v0.0.0-20200318182718-be999391ffa9 h1:GQE1iatYDRrIidq4Zf/9ZzKWyrTk2sXOYc1JADbkAjQ=
|
||||
github.com/knz/strtime v0.0.0-20200318182718-be999391ffa9/go.mod h1:4ZxfWkxwtc7dBeifERVVWRy9F9rTU9p0yCDgeCtlius=
|
||||
github.com/knz/strtime v0.0.0-20200924090105-187c67f2bf5e h1:ViPE0JEOvtw5I0EGUiFSr2VNKGNU+3oBT+oHbDXHbxk=
|
||||
github.com/knz/strtime v0.0.0-20200924090105-187c67f2bf5e/go.mod h1:4ZxfWkxwtc7dBeifERVVWRy9F9rTU9p0yCDgeCtlius=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg=
|
||||
github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
|
||||
github.com/labstack/gommon v0.2.9 h1:heVeuAYtevIQVYkGj6A41dtfT91LrvFG220lavpWhrU=
|
||||
github.com/labstack/gommon v0.2.9/go.mod h1:E8ZTmW9vw5az5/ZyHWCp0Lw4OH2ecsaBP1C/NKavGG4=
|
||||
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
|
||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
@@ -82,16 +162,33 @@ github.com/lithammer/shortuuid v1.0.0 h1:kdcbvjGVEgqeVeDIRtnANOi/F6ftbKrtbxY+cjQ
|
||||
github.com/lithammer/shortuuid v3.0.0+incompatible h1:NcD0xWW/MZYXEHa6ITy6kaXN5nwm/V115vj2YXfhS0w=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.4 h1:uj4xhotfY92Y1Oa6n6HUiFn87CdoEHYUlTy0+IgbLrs=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.4/go.mod h1:RviRjexKqIzx/7r1peoAITm6m7gnif/h+0zmolKJjzw=
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
|
||||
github.com/logrusorgru/aurora/v3 v3.0.0 h1:R6zcoZZbvVcGMvDCKo45A9U/lzYyzl5NfYIvznmDfE4=
|
||||
github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc=
|
||||
github.com/mailgun/mailgun-go v1.1.1 h1:mjMcm4qz+SbjAYbGJ6DKROViKtO5S0YjpuOUxQfdr2A=
|
||||
github.com/mailgun/mailgun-go v2.0.0+incompatible h1:0FoRHWwMUctnd8KIR3vtZbqdfjpIMxOZgcSa51s8F8o=
|
||||
github.com/mailgun/mailgun-go/v4 v4.1.3 h1:KLa5EZaOMMeyvY/lfAhWxv9ealB3mtUsMz0O9XmTtP0=
|
||||
github.com/mailgun/mailgun-go/v4 v4.1.3/go.mod h1:R9kHUQBptF4iSEjhriCQizplCDwrnDShy8w/iPiOfaM=
|
||||
github.com/mailgun/mailgun-go/v4 v4.2.0 h1:AAt7TwR98Pog7zAYK61SW7ikykFFmCovtix3vvS2cK4=
|
||||
github.com/mailgun/mailgun-go/v4 v4.2.0/go.mod h1:R9kHUQBptF4iSEjhriCQizplCDwrnDShy8w/iPiOfaM=
|
||||
github.com/mailgun/mailgun-go/v4 v4.3.0 h1:9nAF7LI3k6bfDPbMZQMMl63Q8/vs+dr1FUN8eR1XMhk=
|
||||
github.com/mailgun/mailgun-go/v4 v4.3.0/go.mod h1:fWuBI2iaS/pSSyo6+EBpHjatQO3lV8onwqcRy7joSJI=
|
||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM=
|
||||
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
|
||||
github.com/mailru/easyjson v0.7.2 h1:V9ecaZWDYm7v9uJ15RZD6DajMu5sE0hdep0aoDwT9g4=
|
||||
github.com/mailru/easyjson v0.7.2/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mailru/easyjson v0.7.3 h1:M6wcO9gFHCIPynXGu4iA+NMs//FCgFUWR2jxqV3/+Xk=
|
||||
github.com/mailru/easyjson v0.7.3/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
@@ -103,50 +200,186 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLD
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/pdrum/swagger-automation v0.0.0-20190629163613-c8c7c80ba858 h1:lgbJiJQx8bXo+eM88AFdd0VxUvaTLzCBXpK+H9poJ+Y=
|
||||
github.com/pdrum/swagger-automation v0.0.0-20190629163613-c8c7c80ba858/go.mod h1:y02HeaN0visd95W6cEX2NXDv5sCwyqfzucWTdDGEwYY=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 h1:PyYN9JH5jY9j6av01SpfRMb+1DWg/i3MbGOKPxJ2wjM=
|
||||
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E=
|
||||
github.com/swaggo/gin-swagger v1.2.0 h1:YskZXEiv51fjOMTsXrOetAjrMDfFaXD79PEoQBOe2W0=
|
||||
github.com/swaggo/gin-swagger v1.2.0/go.mod h1:qlH2+W7zXGZkczuL+r2nEBR2JTT+/lX05Nn6vPhc7OI=
|
||||
github.com/swaggo/gin-swagger v1.3.0 h1:eOmp7r57oUgZPw2dJOjcGNMse9cvXcI4tTqBcnZtPsI=
|
||||
github.com/swaggo/gin-swagger v1.3.0/go.mod h1:oy1BRA6WvgtCp848lhxce7BnWH4C8Bxa0m5SkWx+cS0=
|
||||
github.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+tv8Y=
|
||||
github.com/swaggo/swag v1.6.7 h1:e8GC2xDllJZr3omJkm9YfmK0Y56+rMO3cg0JBKNz09s=
|
||||
github.com/swaggo/swag v1.6.7/go.mod h1:xDhTyuFIujYiN3DKWC/H/83xcfHp+UE/IzWWampG7Zc=
|
||||
github.com/swaggo/swag v1.6.8 h1:z3ZNcpJs/NLMpZcKqXUsBELmmY2Ocy09JXKx5gu3L4M=
|
||||
github.com/swaggo/swag v1.6.8/go.mod h1:a0IpNeMfGidNOcm2TsqODUh9JHdHu3kxDA0UlGbBKjI=
|
||||
github.com/swaggo/swag v1.6.9 h1:BukKRwZjnEcUxQt7Xgfrt9fpav0hiWw9YimdNO9wssw=
|
||||
github.com/swaggo/swag v1.6.9/go.mod h1:a0IpNeMfGidNOcm2TsqODUh9JHdHu3kxDA0UlGbBKjI=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go v1.1.5-pre/go.mod h1:FwP/aQVg39TXzItUBMwnWp9T9gPQnXw4Poh4/oBQZ/0=
|
||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go v1.1.9 h1:SObrQTaSuP8WOv2WNCj8gECiNSJIUvk3Q7N26c96Gws=
|
||||
github.com/ugorji/go v1.1.9/go.mod h1:chLrngdsg43geAaeId+nXO57YsDdl5OZqd/QtBiD19g=
|
||||
github.com/ugorji/go v1.1.13/go.mod h1:jxau1n+/wyTGLQoCkjok9r5zFa/FxT6eI5HiHKQszjc=
|
||||
github.com/ugorji/go v1.2.0 h1:6eXlzYLLwZwXroJx9NyqbYcbv/d93twiOzQLDewE6qM=
|
||||
github.com/ugorji/go v1.2.0/go.mod h1:1ny++pKMXhLWrwWV5Nf+CbOuZJhMoaFD+0GMFfd8fEc=
|
||||
github.com/ugorji/go/codec v0.0.0-20181022190402-e5e69e061d4f/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ugorji/go/codec v1.1.5-pre/go.mod h1:tULtS6Gy1AE1yCENaw4Vb//HLH5njI2tfCQDUqRd8fI=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/ugorji/go/codec v1.1.9 h1:J/7hhpkQwgypRNvaeh/T5gzJ2gEI/l8S3qyRrdEa1fA=
|
||||
github.com/ugorji/go/codec v1.1.9/go.mod h1:+SWgpdqOgdW5sBaiDfkHilQ1SxQ1hBkq/R+kHfL7Suo=
|
||||
github.com/ugorji/go/codec v1.1.13/go.mod h1:oNVt3Dq+FO91WNQ/9JnHKQP2QJxTzoN7wCBFCq1OeuU=
|
||||
github.com/ugorji/go/codec v1.2.0 h1:As6RccOIlbm9wHuWYMlB30dErcI+4WiKWsYsmPkyrUw=
|
||||
github.com/ugorji/go/codec v1.2.0/go.mod h1:dXvG35r7zTX6QImXOSFhGMmKtX+wJ7VTWzGvYQGIjBs=
|
||||
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
|
||||
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
||||
github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4=
|
||||
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
||||
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
|
||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9 h1:umElSU9WZirRdgu2yFHY0ayQkEnKiOC1TtM3fWXFnoU=
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200923182212-328152dc79b1 h1:Iu68XRPd67wN4aRGGWwwq6bZo/25jR6uu52l/j2KkUE=
|
||||
golang.org/x/net v0.0.0-20200923182212-328152dc79b1/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200927032502-5d4f70055728 h1:5wtQIAulKU5AbLQOkjxl32UufnIOqgBX72pS0AV14H0=
|
||||
golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0 h1:wBouT66WTYFXdxfVdz9sVWARVd/2vfGcmI45D2gj45M=
|
||||
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/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/net v0.0.0-20201031054903-ff519b6c9102 h1:42cLlJJdEh+ySyeUUbEQ5bsTiq8voBeTuweGVkY6Puw=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
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-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-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-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-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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1 h1:sIky/MyNRSHTrdxfsiUSS4WIAMvInbeXljJz+jDjeYE=
|
||||
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed h1:J22ig1FUekjjkmZUM7pTKixYm8DvrYsvrBZdunYeIuQ=
|
||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200929083018-4d22bbb62b3c h1:/h0vtH0PyU0xAoZJVcRw1k0Ng+U0JAy3QDiFmppIlIE=
|
||||
golang.org/x/sys v0.0.0-20200929083018-4d22bbb62b3c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201113233024-12cec1faf1ba h1:xmhUJGQGbxlod18iJGqVEp9cHIPLl7QiX2aA3to708s=
|
||||
golang.org/x/sys v0.0.0-20201113233024-12cec1faf1ba/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59 h1:QjA/9ArTfVTLfEhClDCG7SGrZkZixxWpwNCDiwJfh88=
|
||||
golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200923182640-463111b69878 h1:VUw1+Jf6KJPf82mbTQMia6HCnNMv2BbAipkEZ4KTcqQ=
|
||||
golang.org/x/tools v0.0.0-20200923182640-463111b69878/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20200924182824-0f1c53950d78 h1:3JUoxVhcskhsIDEc7vg0MUUEpmPPN5TfG+E97z/Fn90=
|
||||
golang.org/x/tools v0.0.0-20200924182824-0f1c53950d78/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20200929191002-f1e51e6b9437 h1:XSFqH8m531iIGazX5lrUC9j3slbwsZ1GFByqdUrLqmI=
|
||||
golang.org/x/tools v0.0.0-20200929191002-f1e51e6b9437/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20201008184944-d01b322e6f06 h1:w9ail9jFLaySAm61Zjhciu0LQ5i8YTy2pimlNLx4uuk=
|
||||
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/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/tools v0.0.0-20201103235415-b653051172e4 h1:Qe0EMgvVYb6tmJhJHljCj3gS96hvSTkGNaIzp/ivq10=
|
||||
golang.org/x/tools v0.0.0-20201103235415-b653051172e4/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201113202037-1643af1435f3 h1:7R7+wzd5VuLvCNyHZ/MG511kkoP/DBEzkbh8qUsFbY8=
|
||||
golang.org/x/tools v0.0.0-20201113202037-1643af1435f3/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201114224030-61ea331ec02b h1:Ych5r0Z6MLML1fgf5hTg9p5bV56Xqx9xv9hLgMBATWs=
|
||||
golang.org/x/tools v0.0.0-20201114224030-61ea331ec02b/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-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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
@@ -167,16 +400,26 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
|
||||
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
||||
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
|
||||
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
|
||||
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.60.0 h1:P5ZzC7RJO04094NJYlEnBdFK2wwmnCAy/+7sAzvWs60=
|
||||
gopkg.in/ini.v1 v1.60.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.61.0 h1:LBCdW4FmFYL4s/vDZD1RQYX7oAR6IjujCYgMdbHBR10=
|
||||
gopkg.in/ini.v1 v1.61.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
|
||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
||||
BIN
images/accounts.png
Normal file
BIN
images/accounts.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
BIN
images/admin.png
BIN
images/admin.png
Binary file not shown.
|
Before Width: | Height: | Size: 74 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 106 KiB |
BIN
images/demo.gif
Normal file
BIN
images/demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
images/invites.png
Normal file
BIN
images/invites.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
BIN
images/jfa.gif
BIN
images/jfa.gif
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 MiB |
7
jfapi/go.mod
Normal file
7
jfapi/go.mod
Normal file
@@ -0,0 +1,7 @@
|
||||
module github.com/hrfee/jfa-go/jfapi
|
||||
|
||||
go 1.15
|
||||
|
||||
replace github.com/hrfee/jfa-go/common => ../common
|
||||
|
||||
require github.com/hrfee/jfa-go/common v0.0.0-00010101000000-000000000000
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package jfapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -7,63 +7,57 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hrfee/jfa-go/common"
|
||||
)
|
||||
|
||||
type ServerInfo struct {
|
||||
type serverInfo struct {
|
||||
LocalAddress string `json:"LocalAddress"`
|
||||
Name string `json:"ServerName"`
|
||||
Version string `json:"Version"`
|
||||
Os string `json:"OperatingSystem"`
|
||||
Id string `json:"Id"`
|
||||
OS string `json:"OperatingSystem"`
|
||||
ID string `json:"Id"`
|
||||
}
|
||||
|
||||
// Jellyfin represents a running Jellyfin instance.
|
||||
type Jellyfin struct {
|
||||
server string
|
||||
client string
|
||||
version string
|
||||
device string
|
||||
deviceId string
|
||||
useragent string
|
||||
auth string
|
||||
header map[string]string
|
||||
serverInfo ServerInfo
|
||||
username string
|
||||
password string
|
||||
authenticated bool
|
||||
accessToken string
|
||||
userId string
|
||||
httpClient *http.Client
|
||||
loginParams map[string]string
|
||||
userCache []map[string]interface{}
|
||||
cacheExpiry time.Time
|
||||
cacheLength int
|
||||
noFail bool
|
||||
Server string
|
||||
client string
|
||||
version string
|
||||
device string
|
||||
deviceID string
|
||||
useragent string
|
||||
auth string
|
||||
header map[string]string
|
||||
ServerInfo serverInfo
|
||||
Username string
|
||||
password string
|
||||
Authenticated bool
|
||||
AccessToken string
|
||||
userID string
|
||||
httpClient *http.Client
|
||||
loginParams map[string]string
|
||||
userCache []map[string]interface{}
|
||||
CacheExpiry time.Time
|
||||
cacheLength int
|
||||
noFail bool
|
||||
timeoutHandler common.TimeoutHandler
|
||||
}
|
||||
|
||||
func timeoutHandler(name, addr string, noFail bool) {
|
||||
if r := recover(); r != nil {
|
||||
out := fmt.Sprintf("Failed to authenticate with %s @ %s: Timed out", name, addr)
|
||||
if noFail {
|
||||
log.Printf(out)
|
||||
} else {
|
||||
log.Fatalf(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newJellyfin(server, client, version, device, deviceId string) (*Jellyfin, error) {
|
||||
// NewJellyfin returns a new Jellyfin object.
|
||||
func NewJellyfin(server, client, version, device, deviceID string, timeoutHandler common.TimeoutHandler, cacheTimeout int) (*Jellyfin, error) {
|
||||
jf := &Jellyfin{}
|
||||
jf.server = server
|
||||
jf.Server = server
|
||||
jf.client = client
|
||||
jf.version = version
|
||||
jf.device = device
|
||||
jf.deviceId = deviceId
|
||||
jf.deviceID = deviceID
|
||||
jf.useragent = fmt.Sprintf("%s/%s", client, version)
|
||||
jf.auth = fmt.Sprintf("MediaBrowser Client=%s, Device=%s, DeviceId=%s, Version=%s", client, device, deviceId, version)
|
||||
jf.timeoutHandler = timeoutHandler
|
||||
jf.auth = fmt.Sprintf("MediaBrowser Client=%s, Device=%s, DeviceId=%s, Version=%s", client, device, deviceID, version)
|
||||
jf.header = map[string]string{
|
||||
"Accept": "application/json",
|
||||
"Content-type": "application/json; charset=UTF-8",
|
||||
@@ -76,21 +70,22 @@ func newJellyfin(server, client, version, device, deviceId string) (*Jellyfin, e
|
||||
jf.httpClient = &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
infoUrl := fmt.Sprintf("%s/System/Info/Public", server)
|
||||
req, _ := http.NewRequest("GET", infoUrl, nil)
|
||||
infoURL := fmt.Sprintf("%s/System/Info/Public", server)
|
||||
req, _ := http.NewRequest("GET", infoURL, nil)
|
||||
resp, err := jf.httpClient.Do(req)
|
||||
defer timeoutHandler("Jellyfin", jf.server, jf.noFail)
|
||||
defer jf.timeoutHandler()
|
||||
if err == nil {
|
||||
data, _ := ioutil.ReadAll(resp.Body)
|
||||
json.Unmarshal(data, &jf.serverInfo)
|
||||
json.Unmarshal(data, &jf.ServerInfo)
|
||||
}
|
||||
jf.cacheLength = 30
|
||||
jf.cacheExpiry = time.Now()
|
||||
jf.cacheLength = cacheTimeout
|
||||
jf.CacheExpiry = time.Now()
|
||||
return jf, nil
|
||||
}
|
||||
|
||||
func (jf *Jellyfin) authenticate(username, password string) (map[string]interface{}, int, error) {
|
||||
jf.username = username
|
||||
// Authenticate attempts to authenticate using a username & password
|
||||
func (jf *Jellyfin) Authenticate(username, password string) (map[string]interface{}, int, error) {
|
||||
jf.Username = username
|
||||
jf.password = password
|
||||
jf.loginParams = map[string]string{
|
||||
"Username": username,
|
||||
@@ -105,9 +100,9 @@ func (jf *Jellyfin) authenticate(username, password string) (map[string]interfac
|
||||
return nil, 0, err
|
||||
}
|
||||
// loginParams, _ := json.Marshal(jf.loginParams)
|
||||
url := fmt.Sprintf("%s/Users/authenticatebyname", jf.server)
|
||||
url := fmt.Sprintf("%s/Users/authenticatebyname", jf.Server)
|
||||
req, err := http.NewRequest("POST", url, buffer)
|
||||
defer timeoutHandler("Jellyfin", jf.server, jf.noFail)
|
||||
defer jf.timeoutHandler()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -128,16 +123,16 @@ func (jf *Jellyfin) authenticate(username, password string) (map[string]interfac
|
||||
}
|
||||
var respData map[string]interface{}
|
||||
json.NewDecoder(data).Decode(&respData)
|
||||
jf.accessToken = respData["AccessToken"].(string)
|
||||
jf.AccessToken = respData["AccessToken"].(string)
|
||||
user := respData["User"].(map[string]interface{})
|
||||
jf.userId = respData["User"].(map[string]interface{})["Id"].(string)
|
||||
jf.auth = fmt.Sprintf("MediaBrowser Client=\"%s\", Device=\"%s\", DeviceId=\"%s\", Version=\"%s\", Token=\"%s\"", jf.client, jf.device, jf.deviceId, jf.version, jf.accessToken)
|
||||
jf.userID = respData["User"].(map[string]interface{})["Id"].(string)
|
||||
jf.auth = fmt.Sprintf("MediaBrowser Client=\"%s\", Device=\"%s\", DeviceId=\"%s\", Version=\"%s\", Token=\"%s\"", jf.client, jf.device, jf.deviceID, jf.version, jf.AccessToken)
|
||||
jf.header["X-Emby-Authorization"] = jf.auth
|
||||
jf.authenticated = true
|
||||
jf.Authenticated = true
|
||||
return user, resp.StatusCode, nil
|
||||
}
|
||||
|
||||
func (jf *Jellyfin) _get(url string, params map[string]string) (string, int, error) {
|
||||
func (jf *Jellyfin) get(url string, params map[string]string) (string, int, error) {
|
||||
var req *http.Request
|
||||
if params != nil {
|
||||
jsonParams, _ := json.Marshal(params)
|
||||
@@ -149,13 +144,13 @@ func (jf *Jellyfin) _get(url string, params map[string]string) (string, int, err
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
resp, err := jf.httpClient.Do(req)
|
||||
defer timeoutHandler("Jellyfin", jf.server, jf.noFail)
|
||||
defer jf.timeoutHandler()
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
if resp.StatusCode == 401 && jf.authenticated {
|
||||
jf.authenticated = false
|
||||
_, _, authErr := jf.authenticate(jf.username, jf.password)
|
||||
if resp.StatusCode == 401 && jf.Authenticated {
|
||||
jf.Authenticated = false
|
||||
_, _, authErr := jf.Authenticate(jf.Username, jf.password)
|
||||
if authErr == nil {
|
||||
v1, v2, v3 := jf._get(url, params)
|
||||
v1, v2, v3 := jf.get(url, params)
|
||||
return v1, v2, v3
|
||||
}
|
||||
}
|
||||
@@ -164,9 +159,6 @@ func (jf *Jellyfin) _get(url string, params map[string]string) (string, int, err
|
||||
defer resp.Body.Close()
|
||||
var data io.Reader
|
||||
encoding := resp.Header.Get("Content-Encoding")
|
||||
if TEST {
|
||||
fmt.Println("response encoding:", encoding)
|
||||
}
|
||||
switch encoding {
|
||||
case "gzip":
|
||||
data, _ = gzip.NewReader(resp.Body)
|
||||
@@ -180,20 +172,20 @@ func (jf *Jellyfin) _get(url string, params map[string]string) (string, int, err
|
||||
return buf.String(), resp.StatusCode, nil
|
||||
}
|
||||
|
||||
func (jf *Jellyfin) _post(url string, data map[string]interface{}, response bool) (string, int, error) {
|
||||
func (jf *Jellyfin) post(url string, data map[string]interface{}, response bool) (string, int, error) {
|
||||
params, _ := json.Marshal(data)
|
||||
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(params))
|
||||
for name, value := range jf.header {
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
resp, err := jf.httpClient.Do(req)
|
||||
defer timeoutHandler("Jellyfin", jf.server, jf.noFail)
|
||||
defer jf.timeoutHandler()
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
if resp.StatusCode == 401 && jf.authenticated {
|
||||
jf.authenticated = false
|
||||
_, _, authErr := jf.authenticate(jf.username, jf.password)
|
||||
if resp.StatusCode == 401 && jf.Authenticated {
|
||||
jf.Authenticated = false
|
||||
_, _, authErr := jf.Authenticate(jf.Username, jf.password)
|
||||
if authErr == nil {
|
||||
v1, v2, v3 := jf._post(url, data, response)
|
||||
v1, v2, v3 := jf.post(url, data, response)
|
||||
return v1, v2, v3
|
||||
}
|
||||
}
|
||||
@@ -215,45 +207,48 @@ func (jf *Jellyfin) _post(url string, data map[string]interface{}, response bool
|
||||
return "", resp.StatusCode, nil
|
||||
}
|
||||
|
||||
func (jf *Jellyfin) deleteUser(id string) (int, error) {
|
||||
url := fmt.Sprintf("%s/Users/%s", jf.server, id)
|
||||
// DeleteUser deletes the user corresponding to the provided ID.
|
||||
func (jf *Jellyfin) DeleteUser(id string) (int, error) {
|
||||
url := fmt.Sprintf("%s/Users/%s", jf.Server, id)
|
||||
req, _ := http.NewRequest("DELETE", url, nil)
|
||||
for name, value := range jf.header {
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
resp, err := jf.httpClient.Do(req)
|
||||
defer timeoutHandler("Jellyfin", jf.server, jf.noFail)
|
||||
defer jf.timeoutHandler()
|
||||
return resp.StatusCode, err
|
||||
}
|
||||
|
||||
func (jf *Jellyfin) getUsers(public bool) ([]map[string]interface{}, int, error) {
|
||||
// GetUsers returns all (visible) users on the Jellyfin instance.
|
||||
func (jf *Jellyfin) GetUsers(public bool) ([]map[string]interface{}, int, error) {
|
||||
var result []map[string]interface{}
|
||||
var data string
|
||||
var status int
|
||||
var err error
|
||||
if time.Now().After(jf.cacheExpiry) {
|
||||
if time.Now().After(jf.CacheExpiry) {
|
||||
if public {
|
||||
url := fmt.Sprintf("%s/users/public", jf.server)
|
||||
data, status, err = jf._get(url, nil)
|
||||
url := fmt.Sprintf("%s/users/public", jf.Server)
|
||||
data, status, err = jf.get(url, nil)
|
||||
} else {
|
||||
url := fmt.Sprintf("%s/users", jf.server)
|
||||
data, status, err = jf._get(url, jf.loginParams)
|
||||
url := fmt.Sprintf("%s/users", jf.Server)
|
||||
data, status, err = jf.get(url, jf.loginParams)
|
||||
}
|
||||
if err != nil || status != 200 {
|
||||
return nil, status, err
|
||||
}
|
||||
json.Unmarshal([]byte(data), &result)
|
||||
jf.userCache = result
|
||||
jf.cacheExpiry = time.Now().Add(time.Minute * time.Duration(jf.cacheLength))
|
||||
jf.CacheExpiry = time.Now().Add(time.Minute * time.Duration(jf.cacheLength))
|
||||
return result, status, nil
|
||||
}
|
||||
return jf.userCache, 200, nil
|
||||
}
|
||||
|
||||
func (jf *Jellyfin) userByName(username string, public bool) (map[string]interface{}, int, error) {
|
||||
// UserByName returns the user corresponding to the provided username.
|
||||
func (jf *Jellyfin) UserByName(username string, public bool) (map[string]interface{}, int, error) {
|
||||
var match map[string]interface{}
|
||||
find := func() (map[string]interface{}, int, error) {
|
||||
users, status, err := jf.getUsers(public)
|
||||
users, status, err := jf.GetUsers(public)
|
||||
if err != nil || status != 200 {
|
||||
return nil, status, err
|
||||
}
|
||||
@@ -266,48 +261,49 @@ func (jf *Jellyfin) userByName(username string, public bool) (map[string]interfa
|
||||
}
|
||||
match, status, err := find()
|
||||
if match == nil {
|
||||
jf.cacheExpiry = time.Now()
|
||||
jf.CacheExpiry = time.Now()
|
||||
match, status, err = find()
|
||||
}
|
||||
return match, status, err
|
||||
}
|
||||
|
||||
func (jf *Jellyfin) userById(userId string, public bool) (map[string]interface{}, int, error) {
|
||||
if jf.cacheExpiry.After(time.Now()) {
|
||||
// UserByID returns the user corresponding to the provided ID.
|
||||
func (jf *Jellyfin) UserByID(userID string, public bool) (map[string]interface{}, int, error) {
|
||||
if jf.CacheExpiry.After(time.Now()) {
|
||||
for _, user := range jf.userCache {
|
||||
if user["Id"].(string) == userId {
|
||||
if user["Id"].(string) == userID {
|
||||
return user, 200, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if public {
|
||||
users, status, err := jf.getUsers(public)
|
||||
users, status, err := jf.GetUsers(public)
|
||||
if err != nil || status != 200 {
|
||||
return nil, status, err
|
||||
}
|
||||
for _, user := range users {
|
||||
if user["Id"].(string) == userId {
|
||||
if user["Id"].(string) == userID {
|
||||
return user, status, nil
|
||||
}
|
||||
}
|
||||
return nil, status, err
|
||||
} else {
|
||||
var result map[string]interface{}
|
||||
var data string
|
||||
var status int
|
||||
var err error
|
||||
url := fmt.Sprintf("%s/users/%s", jf.server, userId)
|
||||
data, status, err = jf._get(url, jf.loginParams)
|
||||
if err != nil || status != 200 {
|
||||
return nil, status, err
|
||||
}
|
||||
json.Unmarshal([]byte(data), &result)
|
||||
return result, status, nil
|
||||
}
|
||||
var result map[string]interface{}
|
||||
var data string
|
||||
var status int
|
||||
var err error
|
||||
url := fmt.Sprintf("%s/users/%s", jf.Server, userID)
|
||||
data, status, err = jf.get(url, jf.loginParams)
|
||||
if err != nil || status != 200 {
|
||||
return nil, status, err
|
||||
}
|
||||
json.Unmarshal([]byte(data), &result)
|
||||
return result, status, nil
|
||||
}
|
||||
|
||||
func (jf *Jellyfin) newUser(username, password string) (map[string]interface{}, int, error) {
|
||||
url := fmt.Sprintf("%s/Users/New", jf.server)
|
||||
// NewUser creates a new user with the provided username and password.
|
||||
func (jf *Jellyfin) NewUser(username, password string) (map[string]interface{}, int, error) {
|
||||
url := fmt.Sprintf("%s/Users/New", jf.Server)
|
||||
stringData := map[string]string{
|
||||
"Name": username,
|
||||
"Password": password,
|
||||
@@ -316,7 +312,7 @@ func (jf *Jellyfin) newUser(username, password string) (map[string]interface{},
|
||||
for key, value := range stringData {
|
||||
data[key] = value
|
||||
}
|
||||
response, status, err := jf._post(url, data, true)
|
||||
response, status, err := jf.post(url, data, true)
|
||||
var recv map[string]interface{}
|
||||
json.Unmarshal([]byte(response), &recv)
|
||||
if err != nil || !(status == 200 || status == 204) {
|
||||
@@ -325,24 +321,27 @@ func (jf *Jellyfin) newUser(username, password string) (map[string]interface{},
|
||||
return recv, status, nil
|
||||
}
|
||||
|
||||
func (jf *Jellyfin) setPolicy(userId string, policy map[string]interface{}) (int, error) {
|
||||
url := fmt.Sprintf("%s/Users/%s/Policy", jf.server, userId)
|
||||
_, status, err := jf._post(url, policy, false)
|
||||
// SetPolicy sets the access policy for the user corresponding to the provided ID.
|
||||
func (jf *Jellyfin) SetPolicy(userID string, policy map[string]interface{}) (int, error) {
|
||||
url := fmt.Sprintf("%s/Users/%s/Policy", jf.Server, userID)
|
||||
_, status, err := jf.post(url, policy, false)
|
||||
if err != nil || status != 200 {
|
||||
return status, err
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func (jf *Jellyfin) setConfiguration(userId string, configuration map[string]interface{}) (int, error) {
|
||||
url := fmt.Sprintf("%s/Users/%s/Configuration", jf.server, userId)
|
||||
_, status, err := jf._post(url, configuration, false)
|
||||
// SetConfiguration sets the configuration (part of homescreen layout) for the user corresponding to the provided ID.
|
||||
func (jf *Jellyfin) SetConfiguration(userID string, configuration map[string]interface{}) (int, error) {
|
||||
url := fmt.Sprintf("%s/Users/%s/Configuration", jf.Server, userID)
|
||||
_, status, err := jf.post(url, configuration, false)
|
||||
return status, err
|
||||
}
|
||||
|
||||
func (jf *Jellyfin) getDisplayPreferences(userId string) (map[string]interface{}, int, error) {
|
||||
url := fmt.Sprintf("%s/DisplayPreferences/usersettings?userId=%s&client=emby", jf.server, userId)
|
||||
data, status, err := jf._get(url, nil)
|
||||
// GetDisplayPreferences gets the displayPreferences (part of homescreen layout) for the user corresponding to the provided ID.
|
||||
func (jf *Jellyfin) GetDisplayPreferences(userID string) (map[string]interface{}, int, error) {
|
||||
url := fmt.Sprintf("%s/DisplayPreferences/usersettings?userId=%s&client=emby", jf.Server, userID)
|
||||
data, status, err := jf.get(url, nil)
|
||||
if err != nil || !(status == 204 || status == 200) {
|
||||
return nil, status, err
|
||||
}
|
||||
@@ -354,9 +353,10 @@ func (jf *Jellyfin) getDisplayPreferences(userId string) (map[string]interface{}
|
||||
return displayprefs, status, nil
|
||||
}
|
||||
|
||||
func (jf *Jellyfin) setDisplayPreferences(userId string, displayprefs map[string]interface{}) (int, error) {
|
||||
url := fmt.Sprintf("%s/DisplayPreferences/usersettings?userId=%s&client=emby", jf.server, userId)
|
||||
_, status, err := jf._post(url, displayprefs, false)
|
||||
// SetDisplayPreferences sets the displayPreferences (part of homescreen layout) for the user corresponding to the provided ID.
|
||||
func (jf *Jellyfin) SetDisplayPreferences(userID string, displayprefs map[string]interface{}) (int, error) {
|
||||
url := fmt.Sprintf("%s/DisplayPreferences/usersettings?userId=%s&client=emby", jf.Server, userID)
|
||||
_, status, err := jf.post(url, displayprefs, false)
|
||||
if err != nil || !(status == 204 || status == 200) {
|
||||
return status, err
|
||||
}
|
||||
@@ -1,15 +1,8 @@
|
||||
import subprocess
|
||||
import shutil
|
||||
import os
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument(
|
||||
"-y", "--yes", help="use assumed node bin directory.", action="store_true"
|
||||
)
|
||||
|
||||
|
||||
def runcmd(cmd):
|
||||
if os.name == "nt":
|
||||
@@ -19,27 +12,11 @@ def runcmd(cmd):
|
||||
|
||||
|
||||
local_path = Path(__file__).resolve().parent
|
||||
out = runcmd("npm bin")
|
||||
|
||||
try:
|
||||
node_bin = Path(out[0].decode("utf-8").rstrip())
|
||||
except:
|
||||
node_bin = Path(out.decode("utf-8").rstrip())
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.yes:
|
||||
print(f'assuming npm bin directory "{node_bin}". Is this correct?')
|
||||
if input("[yY/nN]: ").lower() == "n":
|
||||
node_bin = local_path.parent / "node_modules" / ".bin"
|
||||
print(f'this? "{node_bin}"')
|
||||
if input("[yY/nN]: ").lower() == "n":
|
||||
node_bin = input("input bin directory: ")
|
||||
|
||||
for mjml in [f for f in local_path.iterdir() if f.is_file() and "mjml" in f.suffix]:
|
||||
print(f"Compiling {mjml.name}")
|
||||
fname = mjml.with_suffix(".html")
|
||||
runcmd(f'{str(node_bin / "mjml")} {str(mjml)} -o {str(fname)}')
|
||||
runcmd(f"npx mjml {str(mjml)} -o {str(fname)}")
|
||||
if fname.is_file():
|
||||
print("Done.")
|
||||
|
||||
|
||||
239
main.go
239
main.go
@@ -23,7 +23,14 @@ import (
|
||||
"github.com/gin-contrib/pprof"
|
||||
"github.com/gin-contrib/static"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/hrfee/jfa-go/common"
|
||||
_ "github.com/hrfee/jfa-go/docs"
|
||||
"github.com/hrfee/jfa-go/jfapi"
|
||||
"github.com/hrfee/jfa-go/ombi"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
"github.com/logrusorgru/aurora/v3"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
@@ -47,9 +54,9 @@ type appContext struct {
|
||||
jellyfinLogin bool
|
||||
users []User
|
||||
invalidTokens []string
|
||||
jf *Jellyfin
|
||||
authJf *Jellyfin
|
||||
ombi *Ombi
|
||||
jf *jfapi.Jellyfin
|
||||
authJf *jfapi.Jellyfin
|
||||
ombi *ombi.Ombi
|
||||
datePattern string
|
||||
timePattern string
|
||||
storage Storage
|
||||
@@ -60,6 +67,34 @@ type appContext struct {
|
||||
port int
|
||||
version string
|
||||
quit chan os.Signal
|
||||
lang Languages
|
||||
}
|
||||
|
||||
type Languages struct {
|
||||
langFiles []os.FileInfo // Language filenames
|
||||
langOptions []string // Language names
|
||||
chosenIndex int
|
||||
}
|
||||
|
||||
func (app *appContext) loadHTML(router *gin.Engine) {
|
||||
customPath := app.config.Section("files").Key("html_templates").MustString("")
|
||||
templatePath := filepath.Join(app.local_path, "templates")
|
||||
htmlFiles, err := ioutil.ReadDir(templatePath)
|
||||
if err != nil {
|
||||
app.err.Fatalf("Couldn't access template directory: \"%s\"", filepath.Join(app.local_path, "templates"))
|
||||
return
|
||||
}
|
||||
loadFiles := make([]string, len(htmlFiles))
|
||||
for i, f := range htmlFiles {
|
||||
if _, err := os.Stat(filepath.Join(customPath, f.Name())); os.IsNotExist(err) {
|
||||
app.debug.Printf("Using default \"%s\"", f.Name())
|
||||
loadFiles[i] = filepath.Join(templatePath, f.Name())
|
||||
} else {
|
||||
app.info.Printf("Using custom \"%s\"", f.Name())
|
||||
loadFiles[i] = filepath.Join(filepath.Join(customPath, f.Name()))
|
||||
}
|
||||
}
|
||||
router.LoadHTMLFiles(loadFiles...)
|
||||
}
|
||||
|
||||
func GenerateSecret(length int) (string, error) {
|
||||
@@ -108,23 +143,24 @@ var (
|
||||
PORT *int
|
||||
DEBUG *bool
|
||||
TEST bool
|
||||
SWAGGER *bool
|
||||
)
|
||||
|
||||
func test(app *appContext) {
|
||||
fmt.Printf("\n\n----\n\n")
|
||||
settings := map[string]interface{}{
|
||||
"server": app.jf.server,
|
||||
"server version": app.jf.serverInfo.Version,
|
||||
"server name": app.jf.serverInfo.Name,
|
||||
"authenticated?": app.jf.authenticated,
|
||||
"access token": app.jf.accessToken,
|
||||
"username": app.jf.username,
|
||||
"server": app.jf.Server,
|
||||
"server version": app.jf.ServerInfo.Version,
|
||||
"server name": app.jf.ServerInfo.Name,
|
||||
"authenticated?": app.jf.Authenticated,
|
||||
"access token": app.jf.AccessToken,
|
||||
"username": app.jf.Username,
|
||||
}
|
||||
for n, v := range settings {
|
||||
fmt.Println(n, ":", v)
|
||||
}
|
||||
users, status, err := app.jf.getUsers(false)
|
||||
fmt.Printf("getUsers: code %d err %s maplength %d\n", status, err, len(users))
|
||||
users, status, err := app.jf.GetUsers(false)
|
||||
fmt.Printf("GetUsers: code %d err %s maplength %d\n", status, err, len(users))
|
||||
fmt.Printf("View output? [y/n]: ")
|
||||
var choice string
|
||||
fmt.Scanln(&choice)
|
||||
@@ -135,8 +171,8 @@ func test(app *appContext) {
|
||||
fmt.Printf("Enter a user to grab: ")
|
||||
var username string
|
||||
fmt.Scanln(&username)
|
||||
user, status, err := app.jf.userByName(username, false)
|
||||
fmt.Printf("userByName (%s): code %d err %s", username, status, err)
|
||||
user, status, err := app.jf.UserByName(username, false)
|
||||
fmt.Printf("UserByName (%s): code %d err %s", username, status, err)
|
||||
out, err := json.MarshalIndent(user, "", " ")
|
||||
fmt.Print(string(out))
|
||||
}
|
||||
@@ -165,10 +201,23 @@ func start(asDaemon, firstCall bool) {
|
||||
HOST = flag.String("host", "", "alternate address to host web ui on.")
|
||||
PORT = flag.Int("port", 0, "alternate port to host web ui on.")
|
||||
DEBUG = flag.Bool("debug", false, "Enables debug logging and exposes pprof.")
|
||||
SWAGGER = flag.Bool("swagger", false, "Enable swagger at /swagger/index.html")
|
||||
|
||||
flag.Parse()
|
||||
if *SWAGGER {
|
||||
os.Setenv("SWAGGER", "1")
|
||||
}
|
||||
if *DEBUG {
|
||||
os.Setenv("DEBUG", "1")
|
||||
}
|
||||
}
|
||||
|
||||
if os.Getenv("SWAGGER") == "1" {
|
||||
*SWAGGER = true
|
||||
}
|
||||
if os.Getenv("DEBUG") == "1" {
|
||||
*DEBUG = true
|
||||
}
|
||||
// attempt to apply command line flags correctly
|
||||
if app.config_path == *CONFIG && app.data_path != *DATA {
|
||||
app.data_path = *DATA
|
||||
@@ -208,7 +257,8 @@ func start(asDaemon, firstCall bool) {
|
||||
var nConfig *os.File
|
||||
nConfig, err := os.Create(app.config_path)
|
||||
if err != nil {
|
||||
app.err.Fatalf("Couldn't open config file for writing: \"%s\"", app.config_path)
|
||||
app.err.Printf("Couldn't open config file for writing: \"%s\"", app.config_path)
|
||||
app.err.Fatalf("Error: %s", err)
|
||||
}
|
||||
defer nConfig.Close()
|
||||
_, err = io.Copy(nConfig, dConfig)
|
||||
@@ -223,6 +273,12 @@ func start(asDaemon, firstCall bool) {
|
||||
if app.loadConfig() != nil {
|
||||
app.err.Fatalf("Failed to load config file \"%s\"", app.config_path)
|
||||
}
|
||||
lang := app.config.Section("ui").Key("language").MustString("en-us")
|
||||
app.storage.lang.FormPath = filepath.Join(app.local_path, "lang", "form", lang+".json")
|
||||
if _, err := os.Stat(app.storage.lang.FormPath); os.IsNotExist(err) {
|
||||
app.storage.lang.FormPath = filepath.Join(app.local_path, "lang", "form", "en-us.json")
|
||||
}
|
||||
app.storage.loadLang()
|
||||
app.version = app.config.Section("jellyfin").Key("version").String()
|
||||
// read from config...
|
||||
debugMode = app.config.Section("ui").Key("debug").MustBool(false)
|
||||
@@ -231,7 +287,7 @@ func start(asDaemon, firstCall bool) {
|
||||
debugMode = true
|
||||
}
|
||||
if debugMode {
|
||||
app.info.Println("WARNING: Don't use debug mode in production, as it exposes pprof on the network.")
|
||||
app.info.Print(aurora.Magenta("\n\nWARNING: Don't use debug mode in production, as it exposes pprof on the network.\n\n"))
|
||||
app.debug = log.New(os.Stdout, "[DEBUG] ", log.Ltime|log.Lshortfile)
|
||||
} else {
|
||||
app.debug = log.New(ioutil.Discard, "", 0)
|
||||
@@ -312,35 +368,52 @@ func start(asDaemon, firstCall bool) {
|
||||
|
||||
app.debug.Println("Loading storage")
|
||||
|
||||
// app.storage.invite_path = filepath.Join(app.data_path, "invites.json")
|
||||
app.storage.invite_path = app.config.Section("files").Key("invites").String()
|
||||
app.storage.loadInvites()
|
||||
// app.storage.emails_path = filepath.Join(app.data_path, "emails.json")
|
||||
app.storage.emails_path = app.config.Section("files").Key("emails").String()
|
||||
app.storage.loadEmails()
|
||||
// app.storage.policy_path = filepath.Join(app.data_path, "user_template.json")
|
||||
app.storage.policy_path = app.config.Section("files").Key("user_template").String()
|
||||
app.storage.loadPolicy()
|
||||
// app.storage.configuration_path = filepath.Join(app.data_path, "user_configuration.json")
|
||||
app.storage.configuration_path = app.config.Section("files").Key("user_configuration").String()
|
||||
app.storage.loadConfiguration()
|
||||
// app.storage.displayprefs_path = filepath.Join(app.data_path, "user_displayprefs.json")
|
||||
app.storage.displayprefs_path = app.config.Section("files").Key("user_displayprefs").String()
|
||||
app.storage.loadDisplayprefs()
|
||||
|
||||
app.storage.profiles_path = app.config.Section("files").Key("user_profiles").String()
|
||||
app.storage.loadProfiles()
|
||||
|
||||
if !(len(app.storage.policy) == 0 && len(app.storage.configuration) == 0 && len(app.storage.displayprefs) == 0) {
|
||||
app.info.Println("Migrating user template files to new profile format")
|
||||
app.storage.migrateToProfile()
|
||||
for _, path := range [3]string{app.storage.policy_path, app.storage.configuration_path, app.storage.displayprefs_path} {
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
dir, fname := filepath.Split(path)
|
||||
newFname := strings.Replace(fname, ".json", ".old.json", 1)
|
||||
err := os.Rename(path, filepath.Join(dir, newFname))
|
||||
if err != nil {
|
||||
app.err.Fatalf("Failed to rename %s: %s", fname, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
app.info.Println("In case of a problem, your original files have been renamed to <file>.old.json")
|
||||
app.storage.storeProfiles()
|
||||
}
|
||||
|
||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||
app.storage.ombi_path = app.config.Section("files").Key("ombi_template").String()
|
||||
app.storage.loadOmbiTemplate()
|
||||
app.ombi = newOmbi(
|
||||
app.config.Section("ombi").Key("server").String(),
|
||||
ombiServer := app.config.Section("ombi").Key("server").String()
|
||||
app.ombi = ombi.NewOmbi(
|
||||
ombiServer,
|
||||
app.config.Section("ombi").Key("api_key").String(),
|
||||
true,
|
||||
common.NewTimeoutHandler("Ombi", ombiServer, true),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
app.configBase_path = filepath.Join(app.local_path, "config-base.json")
|
||||
config_base, _ := ioutil.ReadFile(app.configBase_path)
|
||||
json.Unmarshal(config_base, &app.configBase)
|
||||
configBase, _ := ioutil.ReadFile(app.configBase_path)
|
||||
json.Unmarshal(configBase, &app.configBase)
|
||||
|
||||
themes := map[string]string{
|
||||
"Jellyfin (Dark)": fmt.Sprintf("bs%d-jf.css", app.bsVersion),
|
||||
@@ -369,23 +442,32 @@ func start(asDaemon, firstCall bool) {
|
||||
}
|
||||
|
||||
server := app.config.Section("jellyfin").Key("server").String()
|
||||
app.jf, _ = newJellyfin(server, "jfa-go", app.version, "hrfee-arch", "hrfee-arch")
|
||||
cacheTimeout := int(app.config.Section("jellyfin").Key("cache_timeout").MustUint(30))
|
||||
app.jf, _ = jfapi.NewJellyfin(
|
||||
server,
|
||||
app.config.Section("jellyfin").Key("client").String(),
|
||||
app.config.Section("jellyfin").Key("version").String(),
|
||||
app.config.Section("jellyfin").Key("device").String(),
|
||||
app.config.Section("jellyfin").Key("device_id").String(),
|
||||
common.NewTimeoutHandler("Jellyfin", server, true),
|
||||
cacheTimeout,
|
||||
)
|
||||
var status int
|
||||
_, status, err = app.jf.authenticate(app.config.Section("jellyfin").Key("username").String(), app.config.Section("jellyfin").Key("password").String())
|
||||
_, status, err = app.jf.Authenticate(app.config.Section("jellyfin").Key("username").String(), app.config.Section("jellyfin").Key("password").String())
|
||||
if status != 200 || err != nil {
|
||||
app.err.Fatalf("Failed to authenticate with Jellyfin @ %s: Code %d", server, status)
|
||||
}
|
||||
app.info.Printf("Authenticated with %s", server)
|
||||
app.authJf, _ = newJellyfin(server, "jfa-go", app.version, "auth", "auth")
|
||||
app.authJf, _ = jfapi.NewJellyfin(server, "jfa-go", app.version, "auth", "auth", common.NewTimeoutHandler("Jellyfin", server, true), cacheTimeout)
|
||||
|
||||
app.loadStrftime()
|
||||
|
||||
validatorConf := ValidatorConf{
|
||||
"characters": app.config.Section("password_validation").Key("min_length").MustInt(0),
|
||||
"uppercase characters": app.config.Section("password_validation").Key("upper").MustInt(0),
|
||||
"lowercase characters": app.config.Section("password_validation").Key("lower").MustInt(0),
|
||||
"numbers": app.config.Section("password_validation").Key("number").MustInt(0),
|
||||
"special characters": app.config.Section("password_validation").Key("special").MustInt(0),
|
||||
"length": app.config.Section("password_validation").Key("min_length").MustInt(0),
|
||||
"uppercase": app.config.Section("password_validation").Key("upper").MustInt(0),
|
||||
"lowercase": app.config.Section("password_validation").Key("lower").MustInt(0),
|
||||
"number": app.config.Section("password_validation").Key("number").MustInt(0),
|
||||
"special": app.config.Section("password_validation").Key("special").MustInt(0),
|
||||
}
|
||||
if !app.config.Section("password_validation").Key("enabled").MustBool(false) {
|
||||
for key := range validatorConf {
|
||||
@@ -421,7 +503,7 @@ func start(asDaemon, firstCall bool) {
|
||||
|
||||
router.Use(gin.Recovery())
|
||||
router.Use(static.Serve("/", static.LocalFile(filepath.Join(app.local_path, "static"), false)))
|
||||
router.LoadHTMLGlob(filepath.Join(app.local_path, "templates", "*"))
|
||||
app.loadHTML(router)
|
||||
router.NoRoute(app.NoRouteHandler)
|
||||
if debugMode {
|
||||
app.debug.Println("Loading pprof")
|
||||
@@ -429,35 +511,45 @@ func start(asDaemon, firstCall bool) {
|
||||
}
|
||||
if !firstRun {
|
||||
router.GET("/", app.AdminPage)
|
||||
router.GET("/getToken", app.getToken)
|
||||
router.GET("/token/login", app.getTokenLogin)
|
||||
router.GET("/token/refresh", app.getTokenRefresh)
|
||||
router.POST("/newUser", app.NewUser)
|
||||
router.Use(static.Serve("/invite/", static.LocalFile(filepath.Join(app.local_path, "static"), false)))
|
||||
router.GET("/invite/:invCode", app.InviteProxy)
|
||||
if *SWAGGER {
|
||||
app.info.Print(aurora.Magenta("\n\nWARNING: Swagger should not be used on a public instance.\n\n"))
|
||||
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||
}
|
||||
api := router.Group("/", app.webAuth())
|
||||
router.POST("/logout", app.Logout)
|
||||
api.POST("/newUserAdmin", app.NewUserAdmin)
|
||||
api.POST("/generateInvite", app.GenerateInvite)
|
||||
api.GET("/getInvites", app.GetInvites)
|
||||
api.POST("/setNotify", app.SetNotify)
|
||||
api.POST("/deleteInvite", app.DeleteInvite)
|
||||
api.POST("/deleteUser", app.DeleteUser)
|
||||
api.GET("/getUsers", app.GetUsers)
|
||||
api.POST("/modifyEmails", app.ModifyEmails)
|
||||
api.POST("/setDefaults", app.SetDefaults)
|
||||
api.POST("/applySettings", app.ApplySettings)
|
||||
api.GET("/getConfig", app.GetConfig)
|
||||
api.POST("/modifyConfig", app.ModifyConfig)
|
||||
api.DELETE("/users", app.DeleteUser)
|
||||
api.GET("/users", app.GetUsers)
|
||||
api.POST("/users", app.NewUserAdmin)
|
||||
api.POST("/invites", app.GenerateInvite)
|
||||
api.GET("/invites", app.GetInvites)
|
||||
api.DELETE("/invites", app.DeleteInvite)
|
||||
api.POST("/invites/profile", app.SetProfile)
|
||||
api.GET("/profiles", app.GetProfiles)
|
||||
api.POST("/profiles/default", app.SetDefaultProfile)
|
||||
api.POST("/profiles", app.CreateProfile)
|
||||
api.DELETE("/profiles", app.DeleteProfile)
|
||||
api.POST("/invites/notify", app.SetNotify)
|
||||
api.POST("/users/emails", app.ModifyEmails)
|
||||
// api.POST("/setDefaults", app.SetDefaults)
|
||||
api.POST("/users/settings", app.ApplySettings)
|
||||
api.GET("/config", app.GetConfig)
|
||||
api.POST("/config", app.ModifyConfig)
|
||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||
api.GET("/getOmbiUsers", app.OmbiUsers)
|
||||
api.POST("/setOmbiDefaults", app.SetOmbiDefaults)
|
||||
api.GET("/ombi/users", app.OmbiUsers)
|
||||
api.POST("/ombi/defaults", app.SetOmbiDefaults)
|
||||
}
|
||||
app.info.Printf("Starting router @ %s", address)
|
||||
} else {
|
||||
router.GET("/", func(gc *gin.Context) {
|
||||
gc.HTML(200, "setup.html", gin.H{})
|
||||
})
|
||||
router.POST("/testJF", app.TestJF)
|
||||
router.POST("/modifyConfig", app.ModifyConfig)
|
||||
router.POST("/jellyfin/test", app.TestJF)
|
||||
router.POST("/config", app.ModifyConfig)
|
||||
app.info.Printf("Loading setup @ %s", address)
|
||||
}
|
||||
|
||||
@@ -507,8 +599,49 @@ func flagPassed(name string) (found bool) {
|
||||
return
|
||||
}
|
||||
|
||||
// @title jfa-go internal API
|
||||
// @version 0.2.0
|
||||
// @description API for the jfa-go frontend
|
||||
// @contact.name Harvey Tindall
|
||||
// @contact.email hrfee@protonmail.ch
|
||||
// @license.name MIT
|
||||
// @license.url https://raw.githubusercontent.com/hrfee/jfa-go/main/LICENSE
|
||||
// @BasePath /
|
||||
|
||||
// @securityDefinitions.apikey Bearer
|
||||
// @in header
|
||||
// @name Authorization
|
||||
|
||||
// @securityDefinitions.basic getTokenAuth
|
||||
// @name getTokenAuth
|
||||
|
||||
// @tag.name Auth
|
||||
// @tag.description --------Get a token here first!--------
|
||||
|
||||
// @tag.name Users
|
||||
// @tag.description Jellyfin user related operations.
|
||||
|
||||
// @tag.name Invites
|
||||
// @tag.description Invite related operations.
|
||||
|
||||
// @tag.name Profiles & Settings
|
||||
// @tag.description Profile and settings related operations.
|
||||
|
||||
// @tag.name Configuration
|
||||
// @tag.description jfa-go settings.
|
||||
|
||||
// @tag.name Ombi
|
||||
// @tag.description Ombi related operations.
|
||||
|
||||
// @tag.name Other
|
||||
// @tag.description Things that dont fit elsewhere.
|
||||
|
||||
func printVersion() {
|
||||
fmt.Print(aurora.Sprintf(aurora.Magenta("jfa-go version: %s (%s)\n"), aurora.BrightWhite(VERSION), aurora.White(COMMIT)))
|
||||
}
|
||||
|
||||
func main() {
|
||||
fmt.Printf("jfa-go version: %s (%s)\n", VERSION, COMMIT)
|
||||
printVersion()
|
||||
folder := "/tmp"
|
||||
if PLATFORM == "windows" {
|
||||
folder = os.Getenv("TEMP")
|
||||
@@ -548,7 +681,7 @@ func main() {
|
||||
RESTART = make(chan bool, 1)
|
||||
start(false, true)
|
||||
for {
|
||||
fmt.Printf("jfa-go version: %s (%s)\n", VERSION, COMMIT)
|
||||
printVersion()
|
||||
start(false, false)
|
||||
}
|
||||
}
|
||||
|
||||
129
models.go
Normal file
129
models.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package main
|
||||
|
||||
type stringResponse struct {
|
||||
Response string `json:"response" example:"message"`
|
||||
Error string `json:"error" example:"errorDescription"`
|
||||
}
|
||||
|
||||
type boolResponse struct {
|
||||
Success bool `json:"success" example:"false"`
|
||||
Error bool `json:"error" example:"true"`
|
||||
}
|
||||
|
||||
type newUserDTO struct {
|
||||
Username string `json:"username" example:"jeff" binding:"required"` // User's username
|
||||
Password string `json:"password" example:"guest" binding:"required"` // User's password
|
||||
Email string `json:"email" example:"jeff@jellyf.in"` // User's email address
|
||||
Code string `json:"code" example:"abc0933jncjkcjj"` // Invite code (required on /newUser)
|
||||
}
|
||||
|
||||
type deleteUserDTO struct {
|
||||
Users []string `json:"users" binding:"required"` // List of usernames to delete
|
||||
Notify bool `json:"notify"` // Whether to notify users of deletion
|
||||
Reason string `json:"reason"` // Account deletion reason (for notification)
|
||||
}
|
||||
|
||||
type generateInviteDTO struct {
|
||||
Days int `json:"days" example:"1"` // Number of days
|
||||
Hours int `json:"hours" example:"2"` // Number of hours
|
||||
Minutes int `json:"minutes" example:"3"` // Number of minutes
|
||||
Email string `json:"email" example:"jeff@jellyf.in"` // Send invite to this address
|
||||
MultipleUses bool `json:"multiple-uses" example:"true"` // Allow multiple uses
|
||||
NoLimit bool `json:"no-limit" example:"false"` // No invite use limit
|
||||
RemainingUses int `json:"remaining-uses" example:"5"` // Remaining invite uses
|
||||
Profile string `json:"profile" example:"DefaultProfile"` // Name of profile to apply on this invite
|
||||
}
|
||||
|
||||
type inviteProfileDTO struct {
|
||||
Invite string `json:"invite" example:"slakdaslkdl2342"` // Invite to apply to
|
||||
Profile string `json:"profile" example:"DefaultProfile"` // Profile to use
|
||||
}
|
||||
|
||||
type profileDTO struct {
|
||||
Admin bool `json:"admin" example:"false"` // Whether profile has admin rights or not
|
||||
LibraryAccess string `json:"libraries" example:"all"` // Number of libraries profile has access to
|
||||
FromUser string `json:"fromUser" example:"jeff"` // The user the profile is based on
|
||||
}
|
||||
|
||||
type getProfilesDTO struct {
|
||||
Profiles map[string]profileDTO `json:"profiles"`
|
||||
DefaultProfile string `json:"default_profile"`
|
||||
}
|
||||
|
||||
type profileChangeDTO struct {
|
||||
Name string `json:"name" example:"DefaultProfile" binding:"required"` // Name of the profile
|
||||
}
|
||||
|
||||
type newProfileDTO struct {
|
||||
Name string `json:"name" example:"DefaultProfile" binding:"required"` // Name of the profile
|
||||
ID string `json:"id" example:"kasdjlaskjd342342" binding:"required"` // ID of user to source settings from
|
||||
Homescreen bool `json:"homescreen" example:"true"` // Whether to store homescreen layout or not
|
||||
}
|
||||
|
||||
type inviteDTO struct {
|
||||
Code string `json:"code" example:"sajdlj23423j23"` // Invite code
|
||||
Days int `json:"days" example:"1"` // Number of days till expiry
|
||||
Hours int `json:"hours" example:"2"` // Number of hours till expiry
|
||||
Minutes int `json:"minutes" example:"3"` // Number of minutes till expiry
|
||||
Created string `json:"created" example:"01/01/20 12:00"` // Date of creation
|
||||
Profile string `json:"profile" example:"DefaultProfile"` // Profile used on this invite
|
||||
UsedBy [][]string `json:"used-by,omitempty"` // Users who have used this invite
|
||||
NoLimit bool `json:"no-limit,omitempty"` // If true, invite can be used any number of times
|
||||
RemainingUses int `json:"remaining-uses,omitempty"` // Remaining number of uses (if applicable)
|
||||
Email string `json:"email,omitempty"` // Email the invite was sent to (if applicable)
|
||||
NotifyExpiry bool `json:"notify-expiry,omitempty"` // Whether to notify the requesting user of expiry or not
|
||||
NotifyCreation bool `json:"notify-creation,omitempty"` // Whether to notify the requesting user of account creation or not
|
||||
}
|
||||
|
||||
type getInvitesDTO struct {
|
||||
Profiles []string `json:"profiles"` // List of profiles (name only)
|
||||
Invites []inviteDTO `json:"invites"` // List of invites
|
||||
}
|
||||
|
||||
// fake DTO, if i actually used this the code would be a lot longer
|
||||
type setNotifyValues map[string]struct {
|
||||
NotifyExpiry bool `json:"notify-expiry,omitempty"` // Whether to notify the requesting user of expiry or not
|
||||
NotifyCreation bool `json:"notify-creation,omitempty"` // Whether to notify the requesting user of account creation or not
|
||||
}
|
||||
|
||||
type setNotifyDTO map[string]setNotifyValues
|
||||
|
||||
type deleteInviteDTO struct {
|
||||
Code string `json:"code" example:"skjadajd43234s"` // Code of invite to delete
|
||||
}
|
||||
|
||||
type respUser struct {
|
||||
ID string `json:"id" example:"fdgsdfg45534fa"` // userID of user
|
||||
Name string `json:"name" example:"jeff"` // Username of user
|
||||
Email string `json:"email,omitempty" example:"jeff@jellyf.in"` // Email address of user (if available)
|
||||
LastActive string `json:"last_active"` // Time of last activity on Jellyfin
|
||||
Admin bool `json:"admin" example:"false"` // Whether or not the user is Administrator
|
||||
}
|
||||
|
||||
type getUsersDTO struct {
|
||||
UserList []respUser `json:"users"`
|
||||
}
|
||||
|
||||
type ombiUser struct {
|
||||
Name string `json:"name,omitempty" example:"jeff"` // Name of Ombi user
|
||||
ID string `json:"id" example:"djgkjdg7dkjfsj8"` // userID of Ombi user
|
||||
}
|
||||
|
||||
type ombiUsersDTO struct {
|
||||
Users []ombiUser `json:"users"`
|
||||
}
|
||||
|
||||
type modifyEmailsDTO map[string]string
|
||||
|
||||
type userSettingsDTO struct {
|
||||
From string `json:"from"` // Whether to apply from "user" or "profile"
|
||||
Profile string `json:"profile"` // Name of profile (if from = "profile")
|
||||
ApplyTo []string `json:"apply_to"` // Users to apply settings to
|
||||
ID string `json:"id"` // ID of user (if from = "user")
|
||||
Homescreen bool `json:"homescreen"` // Whether to apply homescreen layout or not
|
||||
}
|
||||
|
||||
type errorListDTO map[string]map[string]string
|
||||
|
||||
type configDTO map[string]interface{}
|
||||
|
||||
5
ombi/go.mod
Normal file
5
ombi/go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module github.com/hrfee/jfa-go/ombi
|
||||
|
||||
replace github.com/hrfee/jfa-go/common => ../common
|
||||
|
||||
go 1.15
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package ombi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -9,31 +9,40 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hrfee/jfa-go/common"
|
||||
)
|
||||
|
||||
// Ombi represents a running Ombi instance.
|
||||
type Ombi struct {
|
||||
server, key string
|
||||
header map[string]string
|
||||
httpClient *http.Client
|
||||
noFail bool
|
||||
server, key string
|
||||
header map[string]string
|
||||
httpClient *http.Client
|
||||
userCache []map[string]interface{}
|
||||
cacheExpiry time.Time
|
||||
cacheLength int
|
||||
timeoutHandler common.TimeoutHandler
|
||||
}
|
||||
|
||||
func newOmbi(server, key string, noFail bool) *Ombi {
|
||||
// NewOmbi returns an Ombi object.
|
||||
func NewOmbi(server, key string, timeoutHandler common.TimeoutHandler) *Ombi {
|
||||
return &Ombi{
|
||||
server: server,
|
||||
key: key,
|
||||
noFail: noFail,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
header: map[string]string{
|
||||
"ApiKey": key,
|
||||
},
|
||||
cacheLength: 30,
|
||||
cacheExpiry: time.Now(),
|
||||
timeoutHandler: timeoutHandler,
|
||||
}
|
||||
}
|
||||
|
||||
// does a GET and returns the response as an io.reader.
|
||||
func (ombi *Ombi) _getReader(url string, params map[string]string) (string, int, error) {
|
||||
// does a GET and returns the response as a string.
|
||||
func (ombi *Ombi) getJSON(url string, params map[string]string) (string, int, error) {
|
||||
if ombi.key == "" {
|
||||
return "", 401, fmt.Errorf("No API key provided")
|
||||
}
|
||||
@@ -48,7 +57,7 @@ func (ombi *Ombi) _getReader(url string, params map[string]string) (string, int,
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
resp, err := ombi.httpClient.Do(req)
|
||||
defer timeoutHandler("Ombi", ombi.server, ombi.noFail)
|
||||
defer ombi.timeoutHandler()
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
if resp.StatusCode == 401 {
|
||||
return "", 401, fmt.Errorf("Invalid API Key")
|
||||
@@ -72,16 +81,16 @@ func (ombi *Ombi) _getReader(url string, params map[string]string) (string, int,
|
||||
}
|
||||
|
||||
// does a POST and optionally returns response as string. Returns a string instead of an io.reader bcs i couldn't get it working otherwise.
|
||||
func (ombi *Ombi) _post(url string, data map[string]interface{}, response bool) (string, int, error) {
|
||||
func (ombi *Ombi) send(mode string, url string, data map[string]interface{}, response bool) (string, int, error) {
|
||||
responseText := ""
|
||||
params, _ := json.Marshal(data)
|
||||
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(params))
|
||||
req, _ := http.NewRequest(mode, url, bytes.NewBuffer(params))
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
for name, value := range ombi.header {
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
resp, err := ombi.httpClient.Do(req)
|
||||
defer timeoutHandler("Ombi", ombi.server, ombi.noFail)
|
||||
defer ombi.timeoutHandler()
|
||||
if err != nil || !(resp.StatusCode == 200 || resp.StatusCode == 201) {
|
||||
if resp.StatusCode == 401 {
|
||||
return "", 401, fmt.Errorf("Invalid API Key")
|
||||
@@ -107,18 +116,57 @@ func (ombi *Ombi) _post(url string, data map[string]interface{}, response bool)
|
||||
return responseText, resp.StatusCode, nil
|
||||
}
|
||||
|
||||
// gets an ombi user by their ID.
|
||||
func (ombi *Ombi) userByID(id string) (result map[string]interface{}, code int, err error) {
|
||||
resp, code, err := ombi._getReader(fmt.Sprintf("%s/api/v1/Identity/User/%s", ombi.server, id), nil)
|
||||
func (ombi *Ombi) post(url string, data map[string]interface{}, response bool) (string, int, error) {
|
||||
return ombi.send("POST", url, data, response)
|
||||
}
|
||||
|
||||
func (ombi *Ombi) put(url string, data map[string]interface{}, response bool) (string, int, error) {
|
||||
return ombi.send("PUT", url, data, response)
|
||||
}
|
||||
|
||||
// ModifyUser applies the given modified user object to the corresponding user.
|
||||
func (ombi *Ombi) ModifyUser(user map[string]interface{}) (status int, err error) {
|
||||
if _, ok := user["id"]; !ok {
|
||||
err = fmt.Errorf("No ID provided")
|
||||
return
|
||||
}
|
||||
_, status, err = ombi.put(ombi.server+"/api/v1/Identity", user, false)
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteUser deletes the user corresponding to the given ID.
|
||||
func (ombi *Ombi) DeleteUser(id string) (code int, err error) {
|
||||
url := fmt.Sprintf("%s/api/v1/Identity/%s", ombi.server, id)
|
||||
req, _ := http.NewRequest("DELETE", url, nil)
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
for name, value := range ombi.header {
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
resp, err := ombi.httpClient.Do(req)
|
||||
defer ombi.timeoutHandler()
|
||||
return resp.StatusCode, err
|
||||
}
|
||||
|
||||
// UserByID returns the user corresponding to the provided ID.
|
||||
func (ombi *Ombi) UserByID(id string) (result map[string]interface{}, code int, err error) {
|
||||
resp, code, err := ombi.getJSON(fmt.Sprintf("%s/api/v1/Identity/User/%s", ombi.server, id), nil)
|
||||
json.Unmarshal([]byte(resp), &result)
|
||||
return
|
||||
}
|
||||
|
||||
// gets a list of all users.
|
||||
func (ombi *Ombi) getUsers() (result []map[string]interface{}, code int, err error) {
|
||||
resp, code, err := ombi._getReader(fmt.Sprintf("%s/api/v1/Identity/Users", ombi.server), nil)
|
||||
json.Unmarshal([]byte(resp), &result)
|
||||
return
|
||||
// GetUsers returns all users on the Ombi instance.
|
||||
func (ombi *Ombi) GetUsers() ([]map[string]interface{}, int, error) {
|
||||
if time.Now().After(ombi.cacheExpiry) {
|
||||
resp, code, err := ombi.getJSON(fmt.Sprintf("%s/api/v1/Identity/Users", ombi.server), nil)
|
||||
var result []map[string]interface{}
|
||||
json.Unmarshal([]byte(resp), &result)
|
||||
ombi.userCache = result
|
||||
if (code == 200 || code == 204) && err == nil {
|
||||
ombi.cacheExpiry = time.Now().Add(time.Minute * time.Duration(ombi.cacheLength))
|
||||
}
|
||||
return result, code, err
|
||||
}
|
||||
return ombi.userCache, 200, nil
|
||||
}
|
||||
|
||||
// Strip these from a user when saving as a template.
|
||||
@@ -133,9 +181,9 @@ var stripFromOmbi = []string{
|
||||
"userName",
|
||||
}
|
||||
|
||||
// returns a template based on the user corresponding to the provided ID's settings.
|
||||
func (ombi *Ombi) templateByID(id string) (result map[string]interface{}, code int, err error) {
|
||||
result, code, err = ombi.userByID(id)
|
||||
// TemplateByID returns a template based on the user corresponding to the provided ID's settings.
|
||||
func (ombi *Ombi) TemplateByID(id string) (result map[string]interface{}, code int, err error) {
|
||||
result, code, err = ombi.UserByID(id)
|
||||
if err != nil || code != 200 {
|
||||
return
|
||||
}
|
||||
@@ -152,14 +200,14 @@ func (ombi *Ombi) templateByID(id string) (result map[string]interface{}, code i
|
||||
return
|
||||
}
|
||||
|
||||
// creates a new user.
|
||||
func (ombi *Ombi) newUser(username, password, email string, template map[string]interface{}) ([]string, int, error) {
|
||||
// NewUser creates a new user with the given username, password and email address.
|
||||
func (ombi *Ombi) NewUser(username, password, email string, template map[string]interface{}) ([]string, int, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/Identity", ombi.server)
|
||||
user := template
|
||||
user["userName"] = username
|
||||
user["password"] = password
|
||||
user["emailAddress"] = email
|
||||
resp, code, err := ombi._post(url, user, true)
|
||||
resp, code, err := ombi.post(url, user, true)
|
||||
var data map[string]interface{}
|
||||
json.Unmarshal([]byte(resp), &data)
|
||||
if err != nil || code != 200 {
|
||||
29
package-lock.json
generated
29
package-lock.json
generated
@@ -49,6 +49,19 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
|
||||
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
|
||||
},
|
||||
"@types/jquery": {
|
||||
"version": "3.5.3",
|
||||
"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-rcxkfkxnW9nrrn+5gOnKddWO6Mc=",
|
||||
"requires": {
|
||||
"@types/sizzle": "*"
|
||||
}
|
||||
},
|
||||
"@types/sizzle": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npm.taobao.org/@types/sizzle/download/@types/sizzle-2.3.2.tgz",
|
||||
"integrity": "sha1-qBG4wY4rq6t9VCszZYh64uTZ3kc="
|
||||
},
|
||||
"abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
@@ -192,9 +205,9 @@
|
||||
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24="
|
||||
},
|
||||
"bootstrap": {
|
||||
"version": "5.0.0-alpha1",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.0.0-alpha1.tgz",
|
||||
"integrity": "sha512-iwKneP2pLXl8lN0YpnOuOARiNPTzmh/4cw+Un86u4OqrMLuQpyMC7nO07hvivvcg0B/ektJPjuPnS1s+YmRK9A=="
|
||||
"version": "5.0.0-alpha3",
|
||||
"resolved": "https://registry.npm.taobao.org/bootstrap/download/bootstrap-5.0.0-alpha3.tgz?cache=0&sync_timestamp=1605115180548&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fbootstrap%2Fdownload%2Fbootstrap-5.0.0-alpha3.tgz",
|
||||
"integrity": "sha1-9au9OHQDg9a44EEhKu9eoiDn2qo="
|
||||
},
|
||||
"bootstrap4": {
|
||||
"version": "npm:bootstrap@4.5.0",
|
||||
@@ -581,6 +594,11 @@
|
||||
"is-arrayish": "^0.2.1"
|
||||
}
|
||||
},
|
||||
"esbuild": {
|
||||
"version": "0.7.8",
|
||||
"resolved": "https://registry.npm.taobao.org/esbuild/download/esbuild-0.7.8.tgz",
|
||||
"integrity": "sha1-e6wXcYHv4MiTmnsVU2aCr+MtG3M="
|
||||
},
|
||||
"escalade": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.0.2.tgz",
|
||||
@@ -2201,6 +2219,11 @@
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npm.taobao.org/typescript/download/typescript-4.0.3.tgz?cache=0&sync_timestamp=1600584904815&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ftypescript%2Fdownload%2Ftypescript-4.0.3.tgz",
|
||||
"integrity": "sha1-FTu9Ro7wdyXB35x36LRT+NNqu6U="
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.4.10",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz",
|
||||
|
||||
@@ -17,12 +17,15 @@
|
||||
},
|
||||
"homepage": "https://github.com/hrfee/jellyfin-accounts#readme",
|
||||
"dependencies": {
|
||||
"@types/jquery": "^3.5.3",
|
||||
"autoprefixer": "^9.8.5",
|
||||
"bootstrap": "^5.0.0-alpha1",
|
||||
"bootstrap": "^5.0.0-alpha3",
|
||||
"bootstrap4": "npm:bootstrap@^4.5.0",
|
||||
"clean-css-cli": "^4.3.0",
|
||||
"esbuild": "^0.7.8",
|
||||
"lodash": "^4.17.19",
|
||||
"mjml": "^4.6.3",
|
||||
"postcss-cli": "^7.1.1"
|
||||
"postcss-cli": "^7.1.1",
|
||||
"typescript": "^4.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) {
|
||||
}
|
||||
app.info.Printf("New password reset for user \"%s\"", pwr.Username)
|
||||
if currentTime := time.Now(); pwr.Expiry.After(currentTime) {
|
||||
user, status, err := app.jf.userByName(pwr.Username, false)
|
||||
user, status, err := app.jf.UserByName(pwr.Username, false)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
app.err.Printf("Failed to get users from Jellyfin: Code %d", status)
|
||||
app.debug.Printf("Error: %s", err)
|
||||
|
||||
41
pwval.go
41
pwval.go
@@ -1,8 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
@@ -19,23 +17,32 @@ func (vd *Validator) init(criteria ValidatorConf) {
|
||||
vd.criteria = criteria
|
||||
}
|
||||
|
||||
// This isn't used, its for swagger
|
||||
type PasswordValidation struct {
|
||||
Characters bool `json:"length,omitempty"` // Number of characters
|
||||
Lowercase bool `json:"lowercase,omitempty"` // Number of lowercase characters
|
||||
Uppercase bool `json:"uppercase,omitempty"` // Number of uppercase characters
|
||||
Numbers bool `json:"number,omitempty"` // Number of numbers
|
||||
Specials bool `json:"special,omitempty"` // Number of special characters
|
||||
}
|
||||
|
||||
func (vd *Validator) validate(password string) map[string]bool {
|
||||
count := map[string]int{}
|
||||
for key, _ := range vd.criteria {
|
||||
for key := range vd.criteria {
|
||||
count[key] = 0
|
||||
}
|
||||
for _, c := range password {
|
||||
count["characters"] += 1
|
||||
count["length"] += 1
|
||||
if unicode.IsUpper(c) {
|
||||
count["uppercase characters"] += 1
|
||||
count["uppercase"] += 1
|
||||
} else if unicode.IsLower(c) {
|
||||
count["lowercase characters"] += 1
|
||||
count["lowercase"] += 1
|
||||
} else if unicode.IsNumber(c) {
|
||||
count["numbers"] += 1
|
||||
count["number"] += 1
|
||||
} else {
|
||||
for _, s := range vd.specialChars {
|
||||
if c == s {
|
||||
count["special characters"] += 1
|
||||
count["special"] += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,18 +58,12 @@ func (vd *Validator) validate(password string) map[string]bool {
|
||||
return results
|
||||
}
|
||||
|
||||
func (vd *Validator) getCriteria() map[string]string {
|
||||
lines := map[string]string{}
|
||||
for criterion, min := range vd.criteria {
|
||||
if min > 0 {
|
||||
text := fmt.Sprintf("Must have at least %d ", min)
|
||||
if min == 1 {
|
||||
text += strings.TrimSuffix(criterion, "s")
|
||||
} else {
|
||||
text += criterion
|
||||
}
|
||||
lines[criterion] = text
|
||||
func (vd *Validator) getCriteria() ValidatorConf {
|
||||
criteria := ValidatorConf{}
|
||||
for key, num := range vd.criteria {
|
||||
if num != 0 {
|
||||
criteria[key] = num
|
||||
}
|
||||
}
|
||||
return lines
|
||||
return criteria
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
.pageContainer {
|
||||
margin: 5% 20% 5% 20%;
|
||||
margin: 5% 30% 5% 30%;
|
||||
}
|
||||
@media (max-width: 1900px) {
|
||||
.pageContainer {
|
||||
margin: 5% 20% 5% 20%;
|
||||
}
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.pageContainer {
|
||||
@@ -115,3 +120,7 @@ body.modal-open {
|
||||
.unfocused {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.text-monospace {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
@@ -142,4 +142,8 @@ $list-group-action-active-bg: $jf-blue-focus;
|
||||
background-color: $jf-blue-hover;
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
filter: invert(80%);
|
||||
}
|
||||
|
||||
@import "../base.scss";
|
||||
|
||||
@@ -3,15 +3,8 @@ import sass
|
||||
import subprocess
|
||||
import shutil
|
||||
import os
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument(
|
||||
"-y", "--yes", help="use assumed node bin directory.", action="store_true"
|
||||
)
|
||||
|
||||
|
||||
def runcmd(cmd):
|
||||
if os.name == "nt":
|
||||
@@ -21,32 +14,22 @@ def runcmd(cmd):
|
||||
|
||||
|
||||
local_path = Path(__file__).resolve().parent
|
||||
out = runcmd("npm bin")
|
||||
|
||||
try:
|
||||
node_bin = Path(out[0].decode("utf-8").rstrip())
|
||||
except:
|
||||
node_bin = Path(out.decode("utf-8").rstrip())
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.yes:
|
||||
print(f'assuming npm bin directory "{node_bin}". Is this correct?')
|
||||
if input("[yY/nN]: ").lower() == "n":
|
||||
node_bin = local_path.parent / "node_modules" / ".bin"
|
||||
print(f'this? "{node_bin}"')
|
||||
if input("[yY/nN]: ").lower() == "n":
|
||||
node_bin = input("input bin directory: ")
|
||||
|
||||
for bsv in [d for d in local_path.iterdir() if "bs" in d.name]:
|
||||
scss = [(bsv / f"{bsv.name}-jf.scss"), (bsv / f"{bsv.name}.scss")]
|
||||
css = [(bsv / f"{bsv.name}-jf.css"), (bsv / f"{bsv.name}.css")]
|
||||
min_css = [(bsv.parents[1] / "data" / "static" / f"{bsv.name}-jf.css"), (bsv.parents[1] / "data" / "static" / f"{bsv.name}.css")]
|
||||
min_css = [
|
||||
(bsv.parents[1] / "data" / "static" / f"{bsv.name}-jf.css"),
|
||||
(bsv.parents[1] / "data" / "static" / f"{bsv.name}.css"),
|
||||
]
|
||||
for i in range(2):
|
||||
with open(css[i], "w") as f:
|
||||
f.write(
|
||||
sass.compile(
|
||||
filename=str(scss[i].resolve()), output_style="expanded", precision=6, omit_source_map_url=True
|
||||
filename=str(scss[i].resolve()),
|
||||
output_style="expanded",
|
||||
precision=6,
|
||||
omit_source_map_url=True,
|
||||
)
|
||||
)
|
||||
if css[i].exists():
|
||||
@@ -55,29 +38,12 @@ for bsv in [d for d in local_path.iterdir() if "bs" in d.name]:
|
||||
cssPath = str(css[i].resolve())
|
||||
if os.name == "nt":
|
||||
cssPath = cssPath.replace("\\", "/")
|
||||
runcmd(
|
||||
f'{str((node_bin / "postcss").resolve())} {cssPath} --replace --use autoprefixer'
|
||||
)
|
||||
runcmd(f"npx postcss {cssPath} --replace --use autoprefixer")
|
||||
print(f"{scss[i].name}: Prefixed.")
|
||||
runcmd(
|
||||
f'{str((node_bin / "cleancss").resolve())} --level 1 --format breakWith=lf --output {str(min_css[i].resolve())} {str(css[i].resolve())}'
|
||||
f"npx cleancss --level 1 --format breakWith=lf --output {str(min_css[i].resolve())} {str(css[i].resolve())}"
|
||||
)
|
||||
if min_css[i].exists():
|
||||
print(f"{scss[i].name}: Minified and copied to {str(min_css[i].resolve())}.")
|
||||
|
||||
# for v in [("bootstrap", "bs5"), ("bootstrap4", "bs4")]:
|
||||
# new_path = str((local_path.parent / "data" / "static" / (v[1] + ".css")).resolve())
|
||||
# shutil.copy(
|
||||
# str(
|
||||
# (
|
||||
# local_path.parent
|
||||
# / "node_modules"
|
||||
# / v[0]
|
||||
# / "dist"
|
||||
# / "css"
|
||||
# / "bootstrap.min.css"
|
||||
# ).resolve()
|
||||
# ),
|
||||
# new_path,
|
||||
# )
|
||||
# print(f"Copied {v[1]} to {new_path}")
|
||||
print(
|
||||
f"{scss[i].name}: Minified and copied to {str(min_css[i].resolve())}."
|
||||
)
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import subprocess
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def runcmd(cmd):
|
||||
if os.name == "nt":
|
||||
return subprocess.check_output(cmd, shell=True)
|
||||
proc = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE)
|
||||
return proc.communicate()
|
||||
|
||||
|
||||
print('Installing npm packages')
|
||||
|
||||
root_path = Path(__file__).parents[1]
|
||||
if os.name == 'nt':
|
||||
root_path /= 'node_modules'
|
||||
|
||||
runcmd('npm install')
|
||||
|
||||
if (root_path / 'node_modules' / 'cleancss').exists() or (root_path / 'cleancss').exists():
|
||||
print(f'Installed successfully in {str((root_path / "node_modules").resolve())}.')
|
||||
|
||||
|
||||
7
setup.go
7
setup.go
@@ -2,6 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/hrfee/jfa-go/common"
|
||||
"github.com/hrfee/jfa-go/jfapi"
|
||||
)
|
||||
|
||||
type testReq struct {
|
||||
@@ -13,9 +15,8 @@ type testReq struct {
|
||||
func (app *appContext) TestJF(gc *gin.Context) {
|
||||
var req testReq
|
||||
gc.BindJSON(&req)
|
||||
tempjf, _ := newJellyfin(req.Host, "jfa-go-setup", app.version, "auth", "auth")
|
||||
tempjf.noFail = true
|
||||
_, status, err := tempjf.authenticate(req.Username, req.Password)
|
||||
tempjf, _ := jfapi.NewJellyfin(req.Host, "jfa-go-setup", app.version, "auth", "auth", common.NewTimeoutHandler("authJF", req.Host, true), 30)
|
||||
_, status, err := tempjf.Authenticate(req.Username, req.Password)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
app.info.Printf("Auth failed with code %d (%s)", status, err)
|
||||
gc.JSON(401, map[string]bool{"success": false})
|
||||
|
||||
104
storage.go
104
storage.go
@@ -3,18 +3,38 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Storage struct {
|
||||
timePattern string
|
||||
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path string
|
||||
invites Invites
|
||||
emails, policy, configuration, displayprefs, ombi_template map[string]interface{}
|
||||
timePattern string
|
||||
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path string
|
||||
invites Invites
|
||||
profiles map[string]Profile
|
||||
defaultProfile string
|
||||
emails, policy, configuration, displayprefs, ombi_template map[string]interface{}
|
||||
lang Lang
|
||||
}
|
||||
|
||||
type Lang struct {
|
||||
FormPath string
|
||||
Form map[string]interface{}
|
||||
}
|
||||
|
||||
// timePattern: %Y-%m-%dT%H:%M:%S.%f
|
||||
|
||||
type Profile struct {
|
||||
Admin bool `json:"admin,omitempty"`
|
||||
LibraryAccess string `json:"libraries,omitempty"`
|
||||
FromUser string `json:"fromUser,omitempty"`
|
||||
Policy map[string]interface{} `json:"policy,omitempty"`
|
||||
Configuration map[string]interface{} `json:"configuration,omitempty"`
|
||||
Displayprefs map[string]interface{} `json:"displayprefs,omitempty"`
|
||||
Default bool `json:"default,omitempty"`
|
||||
}
|
||||
|
||||
type Invite struct {
|
||||
Created time.Time `json:"created"`
|
||||
NoLimit bool `json:"no-limit"`
|
||||
@@ -23,6 +43,7 @@ type Invite struct {
|
||||
Email string `json:"email"`
|
||||
UsedBy [][]string `json:"used-by"`
|
||||
Notify map[string]map[string]bool `json:"notify"`
|
||||
Profile string `json:"profile"`
|
||||
}
|
||||
|
||||
type Invites map[string]Invite
|
||||
@@ -35,6 +56,22 @@ func (st *Storage) storeInvites() error {
|
||||
return storeJSON(st.invite_path, st.invites)
|
||||
}
|
||||
|
||||
func (st *Storage) loadLang() error {
|
||||
err := loadJSON(st.lang.FormPath, &st.lang.Form)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
strings := st.lang.Form["strings"].(map[string]interface{})
|
||||
validationStrings := strings["validationStrings"].(map[string]interface{})
|
||||
vS, err := json.Marshal(validationStrings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
strings["validationStrings"] = string(vS)
|
||||
st.lang.Form["strings"] = strings
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *Storage) loadEmails() error {
|
||||
return loadJSON(st.emails_path, &st.emails)
|
||||
}
|
||||
@@ -75,6 +112,59 @@ func (st *Storage) storeOmbiTemplate() error {
|
||||
return storeJSON(st.ombi_path, st.ombi_template)
|
||||
}
|
||||
|
||||
func (st *Storage) loadProfiles() error {
|
||||
err := loadJSON(st.profiles_path, &st.profiles)
|
||||
for name, profile := range st.profiles {
|
||||
if profile.Default {
|
||||
st.defaultProfile = name
|
||||
}
|
||||
change := false
|
||||
if profile.Policy["IsAdministrator"] != nil {
|
||||
profile.Admin = profile.Policy["IsAdministrator"].(bool)
|
||||
change = true
|
||||
}
|
||||
if profile.Policy["EnabledFolders"] != nil {
|
||||
length := len(profile.Policy["EnabledFolders"].([]interface{}))
|
||||
if length == 0 {
|
||||
profile.LibraryAccess = "All"
|
||||
} else {
|
||||
profile.LibraryAccess = strconv.Itoa(length)
|
||||
}
|
||||
change = true
|
||||
}
|
||||
if profile.FromUser == "" {
|
||||
profile.FromUser = "Unknown"
|
||||
change = true
|
||||
}
|
||||
if change {
|
||||
st.profiles[name] = profile
|
||||
}
|
||||
}
|
||||
if st.defaultProfile == "" {
|
||||
for n := range st.profiles {
|
||||
st.defaultProfile = n
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (st *Storage) storeProfiles() error {
|
||||
return storeJSON(st.profiles_path, st.profiles)
|
||||
}
|
||||
|
||||
func (st *Storage) migrateToProfile() error {
|
||||
st.loadPolicy()
|
||||
st.loadConfiguration()
|
||||
st.loadDisplayprefs()
|
||||
st.loadProfiles()
|
||||
st.profiles["Default"] = Profile{
|
||||
Policy: st.policy,
|
||||
Configuration: st.configuration,
|
||||
Displayprefs: st.displayprefs,
|
||||
}
|
||||
return st.storeProfiles()
|
||||
}
|
||||
|
||||
func loadJSON(path string, obj interface{}) error {
|
||||
var file []byte
|
||||
var err error
|
||||
@@ -83,6 +173,9 @@ func loadJSON(path string, obj interface{}) error {
|
||||
file = []byte("{}")
|
||||
}
|
||||
err = json.Unmarshal(file, &obj)
|
||||
if err != nil {
|
||||
log.Printf("ERROR: Failed to read \"%s\": %s", path, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -92,5 +185,8 @@ func storeJSON(path string, obj interface{}) error {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(path, data, 0644)
|
||||
if err != nil {
|
||||
log.Printf("ERROR: Failed to write to \"%s\": %s", path, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
256
ts/accounts.ts
Normal file
256
ts/accounts.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
import { checkCheckboxes, populateUsers, populateRadios } from "./modules/accounts.js";
|
||||
import { _post, _get, _delete, rmAttr, addAttr } from "./modules/common.js";
|
||||
import { populateProfiles } from "./modules/settings.js";
|
||||
import { Focus, Unfocus, createEl, storeDefaults } from "./modules/admin.js";
|
||||
|
||||
interface aWindow extends Window {
|
||||
changeEmail(icon: HTMLElement, id: string): void;
|
||||
}
|
||||
|
||||
declare var window: aWindow;
|
||||
|
||||
const validateEmail = (email: string): boolean => /\S+@\S+\.\S+/.test(email);
|
||||
|
||||
window.changeEmail = (icon: HTMLElement, id: string): void => {
|
||||
const iconContent = icon.outerHTML;
|
||||
icon.setAttribute('class', '');
|
||||
const entry = icon.nextElementSibling as HTMLInputElement;
|
||||
const ogEmail = entry.value;
|
||||
entry.readOnly = false;
|
||||
entry.classList.remove('form-control-plaintext');
|
||||
entry.classList.add('form-control');
|
||||
if (ogEmail == "") {
|
||||
entry.placeholder = 'Address';
|
||||
}
|
||||
const tick = createEl(`
|
||||
<i class="fa fa-check d-inline-block icon-button text-success" style="margin-left: 0.5rem; margin-right: 0.5rem;"></i>
|
||||
`);
|
||||
tick.onclick = (): void => {
|
||||
const newEmail = entry.value;
|
||||
if (!validateEmail(newEmail) || newEmail == ogEmail) {
|
||||
return;
|
||||
}
|
||||
cross.remove();
|
||||
const spinner = createEl(`
|
||||
<div class="spinner-border spinner-border-sm" role="status" style="width: 1rem; height: 1rem; margin-left: 0.5rem;">
|
||||
<span class="sr-only">Saving...</span>
|
||||
</div>
|
||||
`);
|
||||
tick.replaceWith(spinner);
|
||||
let send = {};
|
||||
send[id] = newEmail;
|
||||
_post("/users/emails", send, function (): void {
|
||||
if (this.readyState == 4) {
|
||||
if (this.status == 200 || this.status == 204) {
|
||||
entry.nextElementSibling.remove();
|
||||
} else {
|
||||
entry.value = ogEmail;
|
||||
}
|
||||
}
|
||||
});
|
||||
icon.outerHTML = iconContent;
|
||||
entry.readOnly = true;
|
||||
entry.classList.remove('form-control');
|
||||
entry.classList.add('form-control-plaintext');
|
||||
entry.placeholder = '';
|
||||
};
|
||||
const cross = createEl(`
|
||||
<i class="fa fa-close d-inline-block icon-button text-danger"></i>
|
||||
`);
|
||||
cross.onclick = (): void => {
|
||||
tick.remove();
|
||||
cross.remove();
|
||||
icon.outerHTML = iconContent;
|
||||
entry.readOnly = true;
|
||||
entry.classList.remove('form-control');
|
||||
entry.classList.add('form-control-plaintext');
|
||||
entry.placeholder = '';
|
||||
entry.value = ogEmail;
|
||||
};
|
||||
icon.parentNode.appendChild(tick);
|
||||
icon.parentNode.appendChild(cross);
|
||||
};
|
||||
|
||||
(<HTMLInputElement>document.getElementById('selectAll')).onclick = function (): void {
|
||||
const checkboxes: NodeListOf<HTMLInputElement> = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]');
|
||||
for (let i = 0; i < checkboxes.length; i++) {
|
||||
checkboxes[i].checked = (<HTMLInputElement>this).checked;
|
||||
}
|
||||
checkCheckboxes();
|
||||
};
|
||||
|
||||
(<HTMLInputElement>document.getElementById('deleteModalNotify')).onclick = function (): void {
|
||||
const textbox: HTMLElement = document.getElementById('deleteModalReasonBox');
|
||||
if ((<HTMLInputElement>this).checked) {
|
||||
Focus(textbox);
|
||||
} else {
|
||||
Unfocus(textbox);
|
||||
}
|
||||
};
|
||||
|
||||
(<HTMLButtonElement>document.getElementById('accountsTabDelete')).onclick = function (): void {
|
||||
const deleteButton = this as HTMLButtonElement;
|
||||
const checkboxes: NodeListOf<HTMLInputElement> = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]:checked');
|
||||
let selected: Array<string> = new Array(checkboxes.length);
|
||||
for (let i = 0; i < checkboxes.length; i++) {
|
||||
selected[i] = checkboxes[i].id.replace("select_", "");
|
||||
}
|
||||
let title = " user";
|
||||
let msg = "Notify user";
|
||||
if (selected.length > 1) {
|
||||
title += "s";
|
||||
msg += "s";
|
||||
}
|
||||
title = `Delete ${selected.length} ${title}`;
|
||||
msg += " of account deletion";
|
||||
|
||||
document.getElementById('deleteModalTitle').textContent = title;
|
||||
const dmNotify = document.getElementById('deleteModalNotify') as HTMLInputElement;
|
||||
dmNotify.checked = false;
|
||||
document.getElementById('deleteModalNotifyLabel').textContent = msg;
|
||||
const dmReason = document.getElementById('deleteModalReason') as HTMLTextAreaElement;
|
||||
dmReason.value = '';
|
||||
Unfocus(document.getElementById('deleteModalReasonBox'));
|
||||
const dmSend = document.getElementById('deleteModalSend') as HTMLButtonElement;
|
||||
dmSend.textContent = 'Delete';
|
||||
dmSend.onclick = function (): void {
|
||||
const button = this as HTMLButtonElement;
|
||||
const send = {
|
||||
'users': selected,
|
||||
'notify': dmNotify.checked,
|
||||
'reason': dmReason.value
|
||||
};
|
||||
_delete("/users", send, function (): void {
|
||||
if (this.readyState == 4) {
|
||||
if (this.status == 500) {
|
||||
if ("error" in this.reponse) {
|
||||
button.textContent = 'Failed';
|
||||
} else {
|
||||
button.textContent = 'Partial fail (check console)';
|
||||
console.log(this.response);
|
||||
}
|
||||
setTimeout((): void => {
|
||||
Unfocus(deleteButton);
|
||||
window.Modals.delete.hide();
|
||||
}, 4000);
|
||||
} else {
|
||||
Unfocus(deleteButton);
|
||||
window.Modals.delete.hide()
|
||||
}
|
||||
populateUsers();
|
||||
checkCheckboxes();
|
||||
}
|
||||
});
|
||||
};
|
||||
window.Modals.delete.show();
|
||||
};
|
||||
|
||||
(<HTMLInputElement>document.getElementById('selectAll')).checked = false;
|
||||
|
||||
(<HTMLButtonElement>document.getElementById('accountsTabSetDefaults')).onclick = function (): void {
|
||||
const checkboxes: NodeListOf<HTMLInputElement> = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]:checked');
|
||||
let userIDs: Array<string> = new Array(checkboxes.length);
|
||||
for (let i = 0; i < checkboxes.length; i++){
|
||||
userIDs[i] = checkboxes[i].id.replace("select_", "");
|
||||
}
|
||||
if (userIDs.length == 0) {
|
||||
return;
|
||||
}
|
||||
populateRadios();
|
||||
let userString = 'user';
|
||||
if (userIDs.length > 1) {
|
||||
userString += "s";
|
||||
}
|
||||
populateProfiles(true);
|
||||
const profileSelect = document.getElementById('profileSelect') as HTMLSelectElement;
|
||||
profileSelect.textContent = '';
|
||||
for (let i = 0; i < window.availableProfiles.length; i++) {
|
||||
profileSelect.innerHTML += `
|
||||
<option value="${window.availableProfiles[i]}" ${(i == 0) ? "selected" : ""}>${window.availableProfiles[i]}</option>
|
||||
`;
|
||||
}
|
||||
document.getElementById('defaultsTitle').textContent = `Apply settings to ${userIDs.length} ${userString}`;
|
||||
document.getElementById('userDefaultsDescription').textContent = `
|
||||
Apply settings from an existing profile or source settings from a user.
|
||||
`;
|
||||
document.getElementById('storeHomescreenLabel').textContent = `Apply homescreen layout`;
|
||||
Focus(document.getElementById('defaultsSourceSection'));
|
||||
(<HTMLSelectElement>document.getElementById('defaultsSource')).value = 'profile';
|
||||
Focus(document.getElementById('profileSelectBox'));
|
||||
Unfocus(document.getElementById('defaultUserRadiosBox'));
|
||||
Unfocus(document.getElementById('newProfileBox'));
|
||||
document.getElementById('storeDefaults').onclick = (): void => storeDefaults(userIDs);
|
||||
window.Modals.userDefaults.show();
|
||||
};
|
||||
|
||||
(<HTMLSelectElement>document.getElementById('defaultsSource')).addEventListener('change', function (): void {
|
||||
const radios = document.getElementById('defaultUserRadiosBox');
|
||||
const profileBox = document.getElementById('profileSelectBox');
|
||||
if (this.value == 'profile') {
|
||||
Unfocus(radios);
|
||||
Focus(profileBox);
|
||||
} else {
|
||||
Unfocus(profileBox);
|
||||
Focus(radios);
|
||||
}
|
||||
});
|
||||
|
||||
(<HTMLButtonElement>document.getElementById('newUserCreate')).onclick = function (): void {
|
||||
const button = this as HTMLButtonElement;
|
||||
const ogText = button.textContent;
|
||||
button.innerHTML = `
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Creating...
|
||||
`;
|
||||
const email: string = (<HTMLInputElement>document.getElementById('newUserEmail')).value;
|
||||
var username: string = email;
|
||||
if (document.getElementById('newUserName') != null) {
|
||||
username = (<HTMLInputElement>document.getElementById('newUserName')).value;
|
||||
}
|
||||
const password: string = (<HTMLInputElement>document.getElementById('newUserPassword')).value;
|
||||
if (!validateEmail(email) && email != "") {
|
||||
return;
|
||||
}
|
||||
const send = {
|
||||
'username': username,
|
||||
'password': password,
|
||||
'email': email
|
||||
};
|
||||
_post("/users", send, function (): void {
|
||||
if (this.readyState == 4) {
|
||||
rmAttr(button, 'btn-primary');
|
||||
if (this.status == 200) {
|
||||
addAttr(button, 'btn-success');
|
||||
button.textContent = 'Success';
|
||||
setTimeout((): void => {
|
||||
rmAttr(button, 'btn-success');
|
||||
addAttr(button, 'btn-primary');
|
||||
button.textContent = ogText;
|
||||
window.Modals.newUser.hide();
|
||||
}, 1000);
|
||||
populateUsers();
|
||||
} else {
|
||||
addAttr(button, 'btn-danger');
|
||||
if ("error" in this.response) {
|
||||
button.textContent = this.response["error"];
|
||||
} else {
|
||||
button.textContent = 'Failed';
|
||||
}
|
||||
setTimeout((): void => {
|
||||
rmAttr(button, 'btn-danger');
|
||||
addAttr(button, 'btn-primary');
|
||||
button.textContent = ogText;
|
||||
}, 2000);
|
||||
populateUsers();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
(<HTMLButtonElement>document.getElementById('accountsTabAddUser')).onclick = function (): void {
|
||||
(<HTMLInputElement>document.getElementById('newUserEmail')).value = '';
|
||||
(<HTMLInputElement>document.getElementById('newUserPassword')).value = '';
|
||||
if (document.getElementById('newUserName') != null) {
|
||||
(<HTMLInputElement>document.getElementById('newUserName')).value = '';
|
||||
}
|
||||
window.Modals.newUser.show();
|
||||
};
|
||||
209
ts/admin.ts
Normal file
209
ts/admin.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import { serializeForm, rmAttr, addAttr, _get, _post, _delete } from "./modules/common.js";
|
||||
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";
|
||||
|
||||
interface aWindow extends Window {
|
||||
toClipboard(str: string): void;
|
||||
}
|
||||
|
||||
declare var window: aWindow;
|
||||
|
||||
interface TabSwitcher {
|
||||
els: Array<HTMLDivElement>;
|
||||
tabButtons: Array<HTMLAnchorElement>;
|
||||
focus: (el: number) => void;
|
||||
invites: () => void;
|
||||
accounts: () => void;
|
||||
settings: () => void;
|
||||
}
|
||||
|
||||
const tabs: TabSwitcher = {
|
||||
els: [document.getElementById('invitesTab') as HTMLDivElement, document.getElementById('accountsTab') as HTMLDivElement, document.getElementById('settingsTab') as HTMLDivElement],
|
||||
tabButtons: [document.getElementById('invitesTabButton') as HTMLAnchorElement, document.getElementById('accountsTabButton') as HTMLAnchorElement, document.getElementById('settingsTabButton') as HTMLAnchorElement],
|
||||
focus: (el: number): void => {
|
||||
for (let i = 0; i < tabs.els.length; i++) {
|
||||
if (i == el) {
|
||||
Focus(tabs.els[i]);
|
||||
addAttr(tabs.tabButtons[i], "active");
|
||||
} else {
|
||||
Unfocus(tabs.els[i]);
|
||||
rmAttr(tabs.tabButtons[i], "active");
|
||||
}
|
||||
}
|
||||
},
|
||||
invites: (): void => tabs.focus(0),
|
||||
accounts: (): void => {
|
||||
populateUsers();
|
||||
(document.getElementById('selectAll') as HTMLInputElement).checked = false;
|
||||
checkCheckboxes();
|
||||
tabs.focus(1);
|
||||
},
|
||||
settings: (): void => openSettings(document.getElementById('settingsSections'), document.getElementById('settingsContent'), (): void => {
|
||||
window.BS.triggerTooltips();
|
||||
showSetting("ui");
|
||||
tabs.focus(2);
|
||||
})
|
||||
};
|
||||
|
||||
window.bsVersion = window.bs5 ? 5 : 4
|
||||
|
||||
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[1].onclick = tabs.accounts;
|
||||
tabs.tabButtons[2].onclick = tabs.settings;
|
||||
|
||||
tabs.invites();
|
||||
|
||||
// Predefined colors for the theme button.
|
||||
var buttonColor: string = "custom";
|
||||
if (window.cssFile.includes("jf")) {
|
||||
buttonColor = "rgb(255,255,255)";
|
||||
} else if (window.cssFile == ("bs" + window.bsVersion + ".css")) {
|
||||
buttonColor = "rgb(16,16,16)";
|
||||
}
|
||||
|
||||
if (buttonColor != "custom") {
|
||||
const switchButton = document.createElement('button') as HTMLButtonElement;
|
||||
switchButton.classList.add('btn', 'btn-secondary');
|
||||
switchButton.innerHTML = `
|
||||
Theme
|
||||
<i class="fa fa-circle circle" style="color: ${buttonColor}; margin-left: 0.4rem;" id="fakeButton"></i>
|
||||
`;
|
||||
switchButton.onclick = (): void => toggleCSS(document.getElementById('fakeButton'));
|
||||
document.getElementById('headerButtons').appendChild(switchButton);
|
||||
}
|
||||
|
||||
var availableProfiles: Array<string>;
|
||||
|
||||
window["token"] = "";
|
||||
|
||||
window.toClipboard = (str: string): void => {
|
||||
const el = document.createElement('textarea') as HTMLTextAreaElement;
|
||||
el.value = str;
|
||||
el.readOnly = true;
|
||||
el.style.position = "absolute";
|
||||
el.style.left = "-9999px";
|
||||
document.body.appendChild(el);
|
||||
const selected = document.getSelection().rangeCount > 0 ? document.getSelection().getRangeAt(0) : false;
|
||||
el.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(el);
|
||||
if (selected) {
|
||||
document.getSelection().removeAllRanges();
|
||||
document.getSelection().addRange(selected);
|
||||
}
|
||||
}
|
||||
|
||||
function login(username: string, password: string, modal: boolean, button?: HTMLButtonElement, run?: (arg0: number) => void): void {
|
||||
const req = new XMLHttpRequest();
|
||||
req.responseType = 'json';
|
||||
let url = "/token/login";
|
||||
const refresh = (username == "" && password == "");
|
||||
if (refresh) {
|
||||
url = "/token/refresh";
|
||||
}
|
||||
req.open("GET", url, true);
|
||||
if (!refresh) {
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(username + ":" + password));
|
||||
}
|
||||
req.onreadystatechange = function (): void {
|
||||
if (this.readyState == 4) {
|
||||
if (this.status != 200) {
|
||||
let errorMsg = this.response["error"];
|
||||
if (!errorMsg) {
|
||||
errorMsg = "Unknown error";
|
||||
}
|
||||
if (modal) {
|
||||
button.disabled = false;
|
||||
button.textContent = errorMsg;
|
||||
addAttr(button, "btn-danger");
|
||||
rmAttr(button, "btn-primary");
|
||||
setTimeout((): void => {
|
||||
addAttr(button, "btn-primary");
|
||||
rmAttr(button, "btn-danger");
|
||||
button.textContent = "Login";
|
||||
}, 4000);
|
||||
} else {
|
||||
window.Modals.login.show();
|
||||
}
|
||||
} else {
|
||||
const data = this.response;
|
||||
window.token = data["token"];
|
||||
generateInvites();
|
||||
setInterval((): void => generateInvites(), 60 * 1000);
|
||||
addOptions(30, document.getElementById('days') as HTMLSelectElement);
|
||||
addOptions(24, document.getElementById('hours') as HTMLSelectElement);
|
||||
const minutes = document.getElementById('minutes') as HTMLSelectElement;
|
||||
addOptions(59, minutes);
|
||||
minutes.value = "30";
|
||||
checkDuration();
|
||||
if (modal) {
|
||||
window.Modals.login.hide();
|
||||
}
|
||||
Focus(document.getElementById('logoutButton'));
|
||||
}
|
||||
if (run) {
|
||||
run(+this.status);
|
||||
}
|
||||
}
|
||||
};
|
||||
req.send();
|
||||
}
|
||||
|
||||
(document.getElementById('loginForm') as HTMLFormElement).onsubmit = function (): boolean {
|
||||
window.token = "";
|
||||
const details = serializeForm('loginForm');
|
||||
const button = document.getElementById('loginSubmit') as HTMLButtonElement;
|
||||
addAttr(button, "btn-primary");
|
||||
rmAttr(button, "btn-danger");
|
||||
button.disabled = true;
|
||||
button.innerHTML = `
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>
|
||||
Loading...`;
|
||||
login(details["username"], details["password"], true, button);
|
||||
return false;
|
||||
};
|
||||
|
||||
generateInvites(true);
|
||||
|
||||
login("", "", false, null, (status: number): void => {
|
||||
if (!(status == 200 || status == 204)) {
|
||||
window.Modals.login.show();
|
||||
}
|
||||
});
|
||||
|
||||
(document.getElementById('logoutButton') as HTMLButtonElement).onclick = function (): void {
|
||||
_post("/logout", null, function (): boolean {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
window.token = "";
|
||||
location.reload();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
153
ts/form.ts
Normal file
153
ts/form.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
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;
|
||||
checkPassword(): void;
|
||||
invalidPassword: string;
|
||||
}
|
||||
|
||||
declare var window: formWindow;
|
||||
|
||||
interface pwValString {
|
||||
singular: string;
|
||||
plural: string;
|
||||
}
|
||||
|
||||
interface pwValStrings {
|
||||
length, uppercase, lowercase, number, special: pwValString;
|
||||
}
|
||||
|
||||
var defaultPwValStrings: pwValStrings = {
|
||||
length: {
|
||||
singular: "Must have at least {n} character",
|
||||
plural: "Must have a least {n} characters"
|
||||
},
|
||||
uppercase: {
|
||||
singular: "Must have at least {n} uppercase character",
|
||||
plural: "Must have at least {n} uppercase characters"
|
||||
},
|
||||
lowercase: {
|
||||
singular: "Must have at least {n} lowercase character",
|
||||
plural: "Must have at least {n} lowercase characters"
|
||||
},
|
||||
number: {
|
||||
singular: "Must have at least {n} number",
|
||||
plural: "Must have at least {n} numbers"
|
||||
},
|
||||
special: {
|
||||
singular: "Must have at least {n} special character",
|
||||
plural: "Must have at least {n} special characters"
|
||||
}
|
||||
}
|
||||
|
||||
const toggleSpinner = (ogText?: string): string => {
|
||||
const submitButton = document.getElementById('submitButton') as HTMLButtonElement;
|
||||
if (document.getElementById('createAccountSpinner')) {
|
||||
submitButton.innerHTML = ogText ? ogText : `<span>Create Account</span>`;
|
||||
submitButton.disabled = false;
|
||||
return "";
|
||||
} else {
|
||||
let ogText = submitButton.innerHTML;
|
||||
submitButton.innerHTML = `
|
||||
<span id="createAccountSpinner" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>Creating...
|
||||
`;
|
||||
return ogText;
|
||||
}
|
||||
};
|
||||
|
||||
for (let key in window.validationStrings) {
|
||||
if (window.validationStrings[key].singular == "" || !(window.validationStrings[key].plural.includes("{n}"))) {
|
||||
window.validationStrings[key].singular = defaultPwValStrings[key].singular;
|
||||
}
|
||||
if (window.validationStrings[key].plural == "" || !(window.validationStrings[key].plural.includes("{n}"))) {
|
||||
window.validationStrings[key].plural = defaultPwValStrings[key].plural;
|
||||
}
|
||||
let el = document.getElementById(key) as HTMLUListElement;
|
||||
if (el) {
|
||||
const min: number = +el.getAttribute("min");
|
||||
let text = "";
|
||||
if (min == 1) {
|
||||
text = window.validationStrings[key].singular.replace("{n}", "1");
|
||||
} else {
|
||||
text = window.validationStrings[key].plural.replace("{n}", min.toString());
|
||||
}
|
||||
(document.getElementById(key).children[0] as HTMLDivElement).textContent = text;
|
||||
}
|
||||
}
|
||||
|
||||
window.BS = window.bs5 ? new BS5 : new BS4;
|
||||
var successBox: BSModal = window.BS.newModal('successBox');;
|
||||
|
||||
var code = window.location.href.split('/').pop();
|
||||
|
||||
(document.getElementById('accountForm') as HTMLFormElement).addEventListener('submit', (event: any): boolean => {
|
||||
event.preventDefault();
|
||||
const el = document.getElementById('errorMessage');
|
||||
if (el) {
|
||||
el.remove();
|
||||
}
|
||||
const ogText = toggleSpinner();
|
||||
let send: Object = serializeForm('accountForm');
|
||||
send["code"] = code;
|
||||
if (!window.usernameEnabled) {
|
||||
send["email"] = send["username"];
|
||||
}
|
||||
_post("/newUser", send, function (): void {
|
||||
if (this.readyState == 4) {
|
||||
toggleSpinner(ogText);
|
||||
let data: Object = this.response;
|
||||
const errorGiven = ("error" in data)
|
||||
if (errorGiven || data["success"] === false) {
|
||||
let errorMessage = "Unknown Error";
|
||||
if (errorGiven && errorGiven != true) {
|
||||
errorMessage = data["error"];
|
||||
}
|
||||
document.getElementById('errorBox').innerHTML += `
|
||||
<button id="errorMessage" class="btn btn-outline-danger" disabled>${errorMessage}</button>
|
||||
`;
|
||||
} else {
|
||||
let valid = true;
|
||||
for (let key in data) {
|
||||
if (data.hasOwnProperty(key)) {
|
||||
const criterion = document.getElementById(key);
|
||||
if (criterion) {
|
||||
if (data[key] === false) {
|
||||
valid = false;
|
||||
addAttr(criterion, "list-group-item-danger");
|
||||
rmAttr(criterion, "list-group-item-success");
|
||||
} else {
|
||||
addAttr(criterion, "list-group-item-success");
|
||||
rmAttr(criterion, "list-group-item-danger");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (valid) {
|
||||
successBox.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
return false;
|
||||
});
|
||||
|
||||
window.checkPassword = (): void => {
|
||||
const entry = document.getElementById('inputPassword') as HTMLInputElement;
|
||||
if (entry.value != "") {
|
||||
const reentry = document.getElementById('reInputPassword') as HTMLInputElement;
|
||||
const identical = (entry.value == reentry.value);
|
||||
const submitButton = document.getElementById('submitButton') as HTMLButtonElement;
|
||||
if (identical) {
|
||||
reentry.setCustomValidity('');
|
||||
rmAttr(submitButton, "btn-outline-danger");
|
||||
addAttr(submitButton, "btn-outline-primary");
|
||||
} else {
|
||||
reentry.setCustomValidity(window.invalidPassword);
|
||||
addAttr(submitButton, "btn-outline-danger");
|
||||
rmAttr(submitButton, "btn-outline-primary");
|
||||
}
|
||||
}
|
||||
}
|
||||
81
ts/invites.ts
Normal file
81
ts/invites.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { serializeForm, rmAttr, addAttr, _get, _post, _delete } from "./modules/common.js";
|
||||
import { generateInvites, checkDuration } from "./modules/invites.js";
|
||||
|
||||
interface aWindow extends Window {
|
||||
setProfile(el: HTMLElement): void;
|
||||
}
|
||||
|
||||
declare var window: aWindow;
|
||||
|
||||
function fixCheckboxes(): void {
|
||||
const send_to_address: Array<HTMLInputElement> = [document.getElementById('send_to_address') as HTMLInputElement, document.getElementById('send_to_address_enabled') as HTMLInputElement];
|
||||
if (send_to_address[0] != null) {
|
||||
send_to_address[0].disabled = !send_to_address[1].checked;
|
||||
}
|
||||
const multiUseEnabled = document.getElementById('multiUseEnabled') as HTMLInputElement;
|
||||
const multiUseCount = document.getElementById('multiUseCount') as HTMLInputElement;
|
||||
const noUseLimit = document.getElementById('noUseLimit') as HTMLInputElement;
|
||||
multiUseCount.disabled = !multiUseEnabled.checked;
|
||||
noUseLimit.checked = false;
|
||||
noUseLimit.disabled = !multiUseEnabled.checked;
|
||||
}
|
||||
|
||||
fixCheckboxes();
|
||||
|
||||
(document.getElementById('inviteForm') as HTMLFormElement).onsubmit = function (): boolean {
|
||||
const button = document.getElementById('generateSubmit') 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 send = serializeForm('inviteForm');
|
||||
send["remaining-uses"] = +send["remaining-uses"];
|
||||
if (!send['multiple-uses'] || send['no-limit']) {
|
||||
delete send['remaining-uses'];
|
||||
}
|
||||
if (send["profile"] == "NoProfile") {
|
||||
send["profile"] = "";
|
||||
}
|
||||
const sendToAddress: any = document.getElementById('send_to_address');
|
||||
const sendToAddressEnabled: any = document.getElementById('send_to_address_enabled');
|
||||
if (sendToAddress && sendToAddressEnabled) {
|
||||
send['email'] = send['send_to_address'];
|
||||
delete send['send_to_address'];
|
||||
delete send['send_to_address_enabled'];
|
||||
}
|
||||
_post("/invites", send, function (): void {
|
||||
if (this.readyState == 4) {
|
||||
button.textContent = 'Generate';
|
||||
button.disabled = false;
|
||||
generateInvites();
|
||||
}
|
||||
});
|
||||
return false;
|
||||
};
|
||||
|
||||
window.BS.triggerTooltips();
|
||||
|
||||
window.setProfile= (select: HTMLSelectElement): void => {
|
||||
if (!select.value) {
|
||||
return;
|
||||
}
|
||||
let val = select.value;
|
||||
if (select.value == "NoProfile") {
|
||||
val = ""
|
||||
}
|
||||
const invite = select.id.replace("profile_", "");
|
||||
const send = {
|
||||
"invite": invite,
|
||||
"profile": val
|
||||
};
|
||||
_post("/invites/profile", send, function (): void {
|
||||
if (this.readyState == 4 && this.status != 200) {
|
||||
generateInvites();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const nE: Array<string> = ["days", "hours", "minutes"];
|
||||
for (const i in nE) {
|
||||
document.getElementById(nE[i]).addEventListener("change", checkDuration);
|
||||
}
|
||||
105
ts/modules/accounts.ts
Normal file
105
ts/modules/accounts.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
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}${admin ? '<span style="margin-left: 1rem;" class="badge rounded-pill bg-info text-dark">Admin</span>' : ''}</td>
|
||||
<td nowrap="nowrap" class="align-middle">${generateEmail(id, name, email)}</td>
|
||||
<td nowrap="nowrap" class="align-middle">${lastActive}</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
68
ts/modules/admin.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
87
ts/modules/animation.ts
Normal file
87
ts/modules/animation.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
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
|
||||
const whichTransitionEvent = (): string => {
|
||||
const el = document.createElement('fakeElement');
|
||||
const transitions = {
|
||||
'transition': 'transitionend',
|
||||
'OTransition': 'oTransitionEnd',
|
||||
'MozTransition': 'transitionend',
|
||||
'WebkitTransition': 'webkitTransitionEnd'
|
||||
};
|
||||
for (const t in transitions) {
|
||||
if (el.style[t] !== undefined) {
|
||||
return transitions[t];
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
var transitionEndEvent = whichTransitionEvent();
|
||||
|
||||
// Toggles between light and dark themes
|
||||
const _toggleCSS = (): void => {
|
||||
const els: NodeListOf<HTMLLinkElement> = document.querySelectorAll('link[rel="stylesheet"][type="text/css"]');
|
||||
let cssEl = 0;
|
||||
let remove = false;
|
||||
if (els.length != 1) {
|
||||
cssEl = 1;
|
||||
remove = true
|
||||
}
|
||||
let href: string = "bs" + window.bsVersion;
|
||||
if (!els[cssEl].href.includes(href + "-jf")) {
|
||||
href += "-jf";
|
||||
}
|
||||
href += ".css";
|
||||
let newEl = els[cssEl].cloneNode(true) as HTMLLinkElement;
|
||||
newEl.href = href;
|
||||
els[cssEl].parentNode.insertBefore(newEl, els[cssEl].nextSibling);
|
||||
if (remove) {
|
||||
els[0].remove();
|
||||
}
|
||||
document.cookie = "css=" + href;
|
||||
}
|
||||
|
||||
// Toggles between light and dark themes, but runs animation if window small enough.
|
||||
window.buttonWidth = 0;
|
||||
export const toggleCSS = (el: HTMLElement): void => {
|
||||
const switchToColor = window.getComputedStyle(document.body, null).backgroundColor;
|
||||
// Max page width for animation to take place
|
||||
let maxWidth = 1500;
|
||||
if (window.innerWidth < maxWidth) {
|
||||
// Calculate minimum radius to cover screen
|
||||
const radius = Math.sqrt(Math.pow(window.innerWidth, 2) + Math.pow(window.innerHeight, 2));
|
||||
const currentRadius = el.getBoundingClientRect().width / 2;
|
||||
const scale = radius / currentRadius;
|
||||
window.buttonWidth = +window.getComputedStyle(el, null).width;
|
||||
document.body.classList.remove('smooth-transition');
|
||||
el.style.transform = `scale(${scale})`;
|
||||
el.style.color = switchToColor;
|
||||
el.addEventListener(transitionEndEvent, function (): void {
|
||||
if (this.style.transform.length != 0) {
|
||||
_toggleCSS();
|
||||
this.style.removeProperty('transform');
|
||||
document.body.classList.add('smooth-transition');
|
||||
}
|
||||
}, false);
|
||||
} else {
|
||||
_toggleCSS();
|
||||
el.style.color = switchToColor;
|
||||
}
|
||||
};
|
||||
|
||||
window.rotateButton = (el: HTMLElement): void => {
|
||||
if (el.classList.contains("rotated")) {
|
||||
rmAttr(el, "rotated")
|
||||
addAttr(el, "not-rotated");
|
||||
} else {
|
||||
rmAttr(el, "not-rotated");
|
||||
addAttr(el, "rotated");
|
||||
}
|
||||
};
|
||||
45
ts/modules/bs4.ts
Normal file
45
ts/modules/bs4.ts
Normal 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
37
ts/modules/bs5.ts
Normal 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);
|
||||
};
|
||||
};
|
||||
77
ts/modules/common.ts
Normal file
77
ts/modules/common.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
declare var window: Window;
|
||||
|
||||
export function serializeForm(id: string): Object {
|
||||
const form = document.getElementById(id) as HTMLFormElement;
|
||||
let formData = {};
|
||||
for (let i = 0; i < form.elements.length; i++) {
|
||||
const el = form.elements[i];
|
||||
if ((el as HTMLInputElement).type == "submit") {
|
||||
continue;
|
||||
}
|
||||
let name = (el as HTMLInputElement).name;
|
||||
if (!name) {
|
||||
name = el.id;
|
||||
}
|
||||
switch ((el as HTMLInputElement).type) {
|
||||
case "checkbox":
|
||||
formData[name] = (el as HTMLInputElement).checked;
|
||||
break;
|
||||
case "text":
|
||||
case "password":
|
||||
case "email":
|
||||
case "number":
|
||||
formData[name] = (el as HTMLInputElement).value;
|
||||
break;
|
||||
case "select-one":
|
||||
case "select":
|
||||
let val: string = (el as HTMLSelectElement).value.toString();
|
||||
if (!isNaN(val as any)) {
|
||||
formData[name] = +val;
|
||||
} else {
|
||||
formData[name] = val;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return formData;
|
||||
}
|
||||
|
||||
export const rmAttr = (el: HTMLElement, attr: string): void => {
|
||||
if (el.classList.contains(attr)) {
|
||||
el.classList.remove(attr);
|
||||
}
|
||||
};
|
||||
|
||||
export const addAttr = (el: HTMLElement, attr: string): void => el.classList.add(attr);
|
||||
|
||||
export const _get = (url: string, data: Object, onreadystatechange: () => void): void => {
|
||||
let req = new XMLHttpRequest();
|
||||
req.open("GET", url, true);
|
||||
req.responseType = 'json';
|
||||
req.setRequestHeader("Authorization", "Bearer " + window.token);
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = onreadystatechange;
|
||||
req.send(JSON.stringify(data));
|
||||
};
|
||||
|
||||
export const _post = (url: string, data: Object, onreadystatechange: () => void, response?: boolean): void => {
|
||||
let req = new XMLHttpRequest();
|
||||
req.open("POST", url, true);
|
||||
if (response) {
|
||||
req.responseType = 'json';
|
||||
}
|
||||
req.setRequestHeader("Authorization", "Bearer " + window.token);
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = onreadystatechange;
|
||||
req.send(JSON.stringify(data));
|
||||
};
|
||||
|
||||
export function _delete(url: string, data: Object, onreadystatechange: () => void): void {
|
||||
let req = new XMLHttpRequest();
|
||||
req.open("DELETE", url, true);
|
||||
req.setRequestHeader("Authorization", "Bearer " + window.token);
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = onreadystatechange;
|
||||
req.send(JSON.stringify(data));
|
||||
}
|
||||
|
||||
297
ts/modules/invites.ts
Normal file
297
ts/modules/invites.ts
Normal 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
164
ts/modules/settings.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
84
ts/ombi.ts
Normal file
84
ts/ombi.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { _get, _post, _delete, rmAttr, addAttr } from "./modules/common.js";
|
||||
|
||||
const ombiDefaultsModal = window.BS.newModal('ombiDefaults');
|
||||
|
||||
(document.getElementById('openOmbiDefaults') as HTMLButtonElement).onclick = function (): void {
|
||||
let button = this as HTMLButtonElement;
|
||||
button.disabled = true;
|
||||
const ogHTML = button.innerHTML;
|
||||
button.innerHTML =
|
||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
||||
'Loading...';
|
||||
_get("/ombi/users", null, function (): void {
|
||||
if (this.readyState == 4) {
|
||||
if (this.status == 200) {
|
||||
const users = this.response['users'];
|
||||
const radioList = document.getElementById('ombiUserRadios');
|
||||
radioList.textContent = '';
|
||||
let first = true;
|
||||
for (const i in users) {
|
||||
const user = users[i];
|
||||
const radio = document.createElement('div') as HTMLDivElement;
|
||||
radio.classList.add('form-check');
|
||||
let checked = '';
|
||||
if (first) {
|
||||
checked = 'checked';
|
||||
first = false;
|
||||
}
|
||||
radio.innerHTML = `
|
||||
<input class="form-check-input" type="radio" name="ombiRadios" id="ombiDefault_${user['id']}" ${checked}>
|
||||
<label class="form-check-label" for="ombiDefault_${user['id']}">${user['name']}</label>
|
||||
`;
|
||||
radioList.appendChild(radio);
|
||||
}
|
||||
button.disabled = false;
|
||||
button.innerHTML = ogHTML;
|
||||
const submitButton = document.getElementById('storeOmbiDefaults') as HTMLButtonElement;
|
||||
submitButton.disabled = false;
|
||||
submitButton.textContent = 'Submit';
|
||||
addAttr(submitButton, "btn-primary");
|
||||
rmAttr(submitButton, "btn-success");
|
||||
rmAttr(submitButton, "btn-danger");
|
||||
ombiDefaultsModal.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
(document.getElementById('storeOmbiDefaults') as HTMLButtonElement).onclick = function (): void {
|
||||
let button = this 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...';
|
||||
const radio = document.querySelector('input[name=ombiRadios]:checked') as HTMLInputElement;
|
||||
const data = {
|
||||
"id": radio.id.replace("ombiDefault_", "")
|
||||
};
|
||||
_post("/ombi/defaults", 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 => ombiDefaultsModal.hide(), 1000);
|
||||
} else {
|
||||
button.textContent = "Failed";
|
||||
rmAttr(button, "btn-primary");
|
||||
addAttr(button, "btn-danger");
|
||||
setTimeout((): void => {
|
||||
button.textContent = "Submit";
|
||||
addAttr(button, "btn-primary");
|
||||
rmAttr(button, "btn-danger");
|
||||
button.disabled = false;
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
205
ts/settings.ts
Normal file
205
ts/settings.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { _post, _get, _delete, rmAttr, addAttr } from "./modules/common.js";
|
||||
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 {
|
||||
window.modifiedConfig["restart-program"] = restart;
|
||||
_post("/config", window.modifiedConfig, function (): void {
|
||||
if (this.readyState == 4) {
|
||||
const save = document.getElementById("settingsSave") as HTMLButtonElement
|
||||
if (this.status == 200 || this.status == 204) {
|
||||
save.textContent = "Success";
|
||||
addAttr(save, "btn-success");
|
||||
rmAttr(save, "btn-primary");
|
||||
setTimeout((): void => {
|
||||
save.textContent = "Save";
|
||||
addAttr(save, "btn-primary");
|
||||
rmAttr(save, "btn-success");
|
||||
}, 1000);
|
||||
} else {
|
||||
save.textContent = "Save";
|
||||
}
|
||||
if (restart) {
|
||||
window.Modals.refresh.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
(document.getElementById('openAbout') as HTMLButtonElement).onclick = (): void => {
|
||||
window.Modals.about.show();
|
||||
};
|
||||
|
||||
(document.getElementById('profiles_button') as HTMLButtonElement).onclick = (): void => showSetting("profiles", populateProfiles);
|
||||
|
||||
window.setDefaultProfile = (name: string): void => _post("/profiles/default", { "name": name }, function (): void {
|
||||
if (this.readyState == 4) {
|
||||
if (this.status != 200) {
|
||||
(document.getElementById(`defaultProfile_${window.availableProfiles[0]}`) as HTMLInputElement).checked = true;
|
||||
(document.getElementById(`defaultProfile_${name}`) as HTMLInputElement).checked = false;
|
||||
} else {
|
||||
generateInvites();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.deleteProfile = (name: string): void => _delete("/profiles", { "name": name }, function (): void {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
populateProfiles();
|
||||
}
|
||||
});
|
||||
|
||||
const createProfile = (): void => _get("/users", null, function (): void {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
window.jfUsers = this.response["users"];
|
||||
populateRadios();
|
||||
const submitButton = document.getElementById('storeDefaults') as HTMLButtonElement;
|
||||
submitButton.disabled = false;
|
||||
submitButton.textContent = 'Create';
|
||||
addAttr(submitButton, "btn-primary");
|
||||
rmAttr(submitButton, "btn-danger");
|
||||
rmAttr(submitButton, "btn-success");
|
||||
document.getElementById('defaultsTitle').textContent = `Create Profile`;
|
||||
document.getElementById('userDefaultsDescription').textContent = `
|
||||
Create an account and configure it to your liking, then choose it from below to store the settings as a profile. Profiles can be specified per invite, so that any new user on that invite will have the settings applied.`;
|
||||
document.getElementById('storeHomescreenLabel').textContent = `Store homescreen layout`;
|
||||
(document.getElementById('defaultsSource') as HTMLSelectElement).value = 'fromUser';
|
||||
document.getElementById('defaultsSourceSection').classList.add('unfocused');
|
||||
(document.getElementById('storeDefaults') as HTMLButtonElement).onclick = storeProfile;
|
||||
Focus(document.getElementById('newProfileBox'));
|
||||
(document.getElementById('newProfileName') as HTMLInputElement).value = '';
|
||||
Focus(document.getElementById('defaultUserRadiosBox'));
|
||||
window.Modals.userDefaults.show();
|
||||
}
|
||||
});
|
||||
|
||||
window.createProfile = createProfile;
|
||||
|
||||
function storeProfile(): void {
|
||||
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;
|
||||
const radio = document.querySelector('input[name=defaultRadios]:checked') as HTMLInputElement
|
||||
const name = (document.getElementById('newProfileName') as HTMLInputElement).value;
|
||||
let id = radio.id.replace("default_", "");
|
||||
let data = {
|
||||
"name": name,
|
||||
"id": id,
|
||||
"homescreen": false
|
||||
}
|
||||
if ((document.getElementById('storeDefaultHomescreen') as HTMLInputElement).checked) {
|
||||
data["homescreen"] = true;
|
||||
}
|
||||
_post("/profiles", 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 = "Create";
|
||||
addAttr(button, "btn-primary");
|
||||
rmAttr(button, "btn-success");
|
||||
button.disabled = false;
|
||||
window.Modals.userDefaults.hide();
|
||||
|
||||
}, 1000);
|
||||
populateProfiles();
|
||||
generateInvites();
|
||||
} 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 = "Create";
|
||||
addAttr(button, "btn-primary");
|
||||
rmAttr(button, "btn-danger");
|
||||
button.disabled = false;
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// (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 {
|
||||
window.modifiedConfig = {};
|
||||
const save = this as HTMLButtonElement;
|
||||
let restartSettingsChanged = false;
|
||||
let settingsChanged = false;
|
||||
for (const i in window.config["order"]) {
|
||||
const section = window.config["order"][i];
|
||||
for (const x in window.config[section]["order"]) {
|
||||
const entry = window.config[section]["order"][x];
|
||||
if (entry == "meta") {
|
||||
continue;
|
||||
}
|
||||
let val: string;
|
||||
const entryID = `${section}_${entry}`;
|
||||
const el = document.getElementById(entryID) as HTMLInputElement;
|
||||
if (el.type == "checkbox") {
|
||||
val = el.checked.toString();
|
||||
} else {
|
||||
val = el.value.toString();
|
||||
}
|
||||
if (val != window.config[section][entry]["value"].toString()) {
|
||||
if (!(section in window.modifiedConfig)) {
|
||||
window.modifiedConfig[section] = {};
|
||||
}
|
||||
window.modifiedConfig[section][entry] = val;
|
||||
settingsChanged = true;
|
||||
if (window.config[section][entry]["requires_restart"]) {
|
||||
restartSettingsChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const spinnerHTML = `
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>
|
||||
Loading...`;
|
||||
if (restartSettingsChanged) {
|
||||
save.innerHTML = spinnerHTML;
|
||||
(document.getElementById('applyRestarts') as HTMLButtonElement).onclick = (): void => sendConfig();
|
||||
const restartButton = document.getElementById('applyAndRestart') as HTMLButtonElement;
|
||||
if (restartButton) {
|
||||
restartButton.onclick = (): void => sendConfig(true);
|
||||
}
|
||||
window.Modals.restart.show();
|
||||
} else if (settingsChanged) {
|
||||
save.innerHTML = spinnerHTML;
|
||||
sendConfig();
|
||||
}
|
||||
};
|
||||
|
||||
(document.getElementById('restartModalCancel') as HTMLButtonElement).onclick = (): void => {
|
||||
document.getElementById('settingsSave').textContent = "Save";
|
||||
};
|
||||
8
ts/tsconfig.json
Normal file
8
ts/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "../data/static",
|
||||
"target": "es6",
|
||||
"lib": ["dom", "es2017"],
|
||||
"typeRoots": ["./node_modules/@types", "./typings"]
|
||||
}
|
||||
}
|
||||
61
ts/typings/d.ts
Normal file
61
ts/typings/d.ts
Normal 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;
|
||||
17
views.go
17
views.go
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -30,23 +31,29 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
||||
// if app.checkInvite(code, false, "") {
|
||||
if _, ok := app.storage.invites[code]; ok {
|
||||
email := app.storage.invites[code].Email
|
||||
gc.HTML(http.StatusOK, "form.html", gin.H{
|
||||
"bs5": app.config.Section("ui").Key("bs5").MustBool(false),
|
||||
if strings.Contains(email, "Failed") {
|
||||
email = ""
|
||||
}
|
||||
gc.HTML(http.StatusOK, "form-loader.html", gin.H{
|
||||
"cssFile": app.cssFile,
|
||||
"contactMessage": app.config.Section("ui").Key("contac_message").String(),
|
||||
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
||||
"helpMessage": app.config.Section("ui").Key("help_message").String(),
|
||||
"successMessage": app.config.Section("ui").Key("success_message").String(),
|
||||
"jfLink": app.config.Section("jellyfin").Key("public_server").String(),
|
||||
"validate": app.config.Section("password_validation").Key("enabled").MustBool(false),
|
||||
"requirements": app.validator.getCriteria(),
|
||||
"email": email,
|
||||
"username": !app.config.Section("email").Key("no_username").MustBool(false),
|
||||
"settings": map[string]bool{
|
||||
"bs5": app.config.Section("ui").Key("bs5").MustBool(false),
|
||||
"username": !app.config.Section("email").Key("no_username").MustBool(false),
|
||||
},
|
||||
"lang": app.storage.lang.Form["strings"],
|
||||
})
|
||||
} else {
|
||||
gc.HTML(404, "invalidCode.html", gin.H{
|
||||
"bs5": app.config.Section("ui").Key("bs5").MustBool(false),
|
||||
"cssFile": app.cssFile,
|
||||
"contactMessage": app.config.Section("ui").Key("contac_message").String(),
|
||||
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user