Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e3d5dac19 | ||
|
|
072776c15f | ||
|
|
1c980cf7cd | ||
|
|
c6f845296a | ||
|
|
a5a721b07c | ||
|
|
086fd0ef2f | ||
|
|
d12335bb4a | ||
|
|
0e39b2b699 | ||
|
|
ee3b421566 | ||
|
|
d144077e62 | ||
|
|
29a79a1ce1 | ||
|
|
681d2ce38d | ||
|
|
cee5489da6 | ||
|
|
b38af84b35 | ||
|
|
8fc9ed1c3c | ||
|
|
6781316474 | ||
|
|
daf190f68b | ||
|
|
56478e96c9 | ||
|
|
ec7609ed8c | ||
|
|
6366239ec4 | ||
|
|
e893c9a234 | ||
|
|
7879fd2581 | ||
|
|
c778837593 | ||
|
|
af47cd9f0b | ||
|
|
151062fbc1 | ||
|
|
abc51f2443 | ||
|
|
8c4bd4541c | ||
|
|
8750efe101 | ||
|
|
252e13757b | ||
|
|
02183c7fcc | ||
|
|
dd0eabf157 | ||
|
|
6436dba48f | ||
|
|
bd8af153a9 | ||
|
|
fd766e7b1a | ||
|
|
fffb3471d6 | ||
|
|
19bd31d968 | ||
|
|
39bf3ad7f1 | ||
|
|
ea5c2b3886 | ||
|
|
8a8fe65192 | ||
|
|
5329f02768 | ||
|
|
7b23545197 | ||
|
|
54af15cc5a | ||
|
|
8ed1662a2f | ||
|
|
23dbcf33ae | ||
|
|
25348a9b1a | ||
|
|
3970cbef3f | ||
|
|
a38d56f362 | ||
|
|
f0be006e16 | ||
|
|
699489e435 | ||
|
|
e576616530 | ||
|
|
05c7b7156b | ||
|
|
c72e1a1c63 |
4
.gitignore
vendored
@@ -9,3 +9,7 @@ data/config-default.ini
|
||||
data/*.html
|
||||
data/*.txt
|
||||
dist/*
|
||||
jfa-go
|
||||
build/
|
||||
pkg/
|
||||
old/
|
||||
|
||||
@@ -5,7 +5,7 @@ release:
|
||||
github:
|
||||
owner: hrfee
|
||||
name: jfa-go
|
||||
name_template: "v{{.Version}} {{.Env.USER}}"
|
||||
name_template: "v{{.Version}}"
|
||||
before:
|
||||
hooks:
|
||||
# You may remove this if you don't use go modules.
|
||||
@@ -14,8 +14,8 @@ before:
|
||||
- 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
|
||||
- python3 mail/generate.py
|
||||
- python3 scss/compile.py -y
|
||||
- python3 mail/generate.py -y
|
||||
builds:
|
||||
- dir: ./
|
||||
env:
|
||||
@@ -24,19 +24,22 @@ builds:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
archives:
|
||||
- replacements:
|
||||
darwin: Darwin
|
||||
linux: Linux
|
||||
windows: Windows
|
||||
386: i386
|
||||
amd64: x86_64
|
||||
files:
|
||||
- data/*
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-next"
|
||||
name_template: "{{ .Tag }}-testing"
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
|
||||
20
Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
FROM golang:latest AS build
|
||||
|
||||
COPY . /opt/build
|
||||
|
||||
RUN apt update -y \
|
||||
&& apt install build-essential python3-pip curl software-properties-common sed -y \
|
||||
&& (curl -sL https://deb.nodesource.com/setup_14.x | bash -) \
|
||||
&& apt install nodejs \
|
||||
&& (cd /opt/build; make headless) \
|
||||
&& sed -i 's#id="pwrJfPath" placeholder="Folder"#id="pwrJfPath" value="/jf" disabled#g' /opt/build/build/data/templates/setup.html
|
||||
|
||||
FROM golang:latest
|
||||
|
||||
COPY --from=build /opt/build/build /opt/jfa-go
|
||||
|
||||
EXPOSE 8056
|
||||
|
||||
CMD [ "/opt/jfa-go/jfa-go", "-data", "/data" ]
|
||||
|
||||
|
||||
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Harvey Tindall
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
49
Makefile
Normal file
@@ -0,0 +1,49 @@
|
||||
configuration:
|
||||
echo "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
|
||||
|
||||
sass:
|
||||
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
|
||||
|
||||
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"
|
||||
python3 mail/generate.py
|
||||
|
||||
compile:
|
||||
echo "Downloading deps"
|
||||
go mod download
|
||||
echo "Building"
|
||||
mkdir -p build
|
||||
go build -o build/jfa-go *.go
|
||||
|
||||
copy:
|
||||
echo "Copying data"
|
||||
cp -r data build/
|
||||
|
||||
install:
|
||||
cp -r build $(DESTDIR)/jfa-go
|
||||
|
||||
all: configuration sass mail compile copy
|
||||
headless: configuration sass-headless mail-headless copy
|
||||
|
||||
|
||||
|
||||
80
README.md
@@ -1,11 +1,83 @@
|
||||
# jfa-go
|
||||
# 
|
||||
|
||||
A rewrite of [jellyfin-accounts](https://github.com/hrfee/jellyfin-accounts) in Go. Should be fully functional, and functions the same as jf-accounts. To switch, copy your existing `~/.jf-accounts` to:
|
||||
jfa-go is a user management app for [Jellyfin](https://github.com/jellyfin/jellyfin) that provides invite-based account creation as well as other features that make one's instance much easier to manage.
|
||||
|
||||
I chose to rewrite the python [jellyfin-accounts](https://github.com/hrfee/jellyfin-accounts) in Go mainly as a learning experience, but also to slightly improve speeds and efficiency.
|
||||
|
||||
#### Features
|
||||
* 🧑 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.
|
||||
* Password validation: Ensure users choose a strong password.
|
||||
* 📨 Email storage: Add your existing user's email addresses through the UI, and jfa-go will ask new users for them on account creation.
|
||||
* Email addresses can optionally be used instead of usernames
|
||||
* 🔑 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.
|
||||
* Authentication via Jellyfin: Instead of using separate credentials for jfa-go and Jellyfin, jfa-go can use it as the authentication provider.
|
||||
* Enables the usage of jfa-go by multiple people
|
||||
* 🌓 Customizable look
|
||||
* Specify contact and help messages to appear in emails and pages
|
||||
* Light and dark themes available
|
||||
* Optionally provide custom CSS
|
||||
|
||||
## Interface
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/hrfee/jellyfin-accounts/main/images/jfa.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>
|
||||
</p>
|
||||
|
||||
#### Install
|
||||
|
||||
Available on the AUR as [jfa-go](https://aur.archlinux.org/packages/jfa-go/).
|
||||
|
||||
For other platforms, grab an archive from the release section for your platform, 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.
|
||||
|
||||
Run the executable to start.
|
||||
|
||||
For [docker](https://hub.docker.com/repository/docker/hrfee/jfa-go), run:
|
||||
```
|
||||
docker create \
|
||||
--name "jfa-go" \ # Whatever you want to name it
|
||||
-p 8056:8056 \
|
||||
-v /path/to/.config/jfa-go:/data \ # Equivalent of ~/.jf-accounts
|
||||
-v /path/to/jellyfin:/jf \ # Path to jellyfin config directory
|
||||
-v /etc/localtime:/etc/localtime:ro \ # Makes sure time is correct
|
||||
hrfee/jfa-go
|
||||
```
|
||||
|
||||
#### 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.
|
||||
|
||||
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.
|
||||
|
||||
Note: jfa-go does not run as a daemon by default. You'll need to figure this out yourself.
|
||||
|
||||
```
|
||||
Usage of ./jfa-go:
|
||||
-config string
|
||||
alternate path to config file. (default "~/.config/jfa-go/config.ini")
|
||||
-data string
|
||||
alternate path to data directory. (default "~/.config/jfa-go")
|
||||
-host string
|
||||
alternate address to host web ui on.
|
||||
-port int
|
||||
alternate port to host web ui on.
|
||||
```
|
||||
|
||||
If you're switching from jellyfin-accounts, copy your existing `~/.jf-accounts` to:
|
||||
|
||||
* `XDG_CONFIG_DIR/jfa-go` (usually ~/.config) on \*nix systems,
|
||||
* `%AppData%/jfa-go` on Windows,
|
||||
* `~/Library/Application Support/jfa-go` on macOS.
|
||||
|
||||
(*or specify config/data path with `-config/-data` respectively.*)
|
||||
|
||||
Suggestions and help welcome.
|
||||
|
||||
380
api.go
@@ -2,33 +2,34 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/knz/strtime"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
"gopkg.in/ini.v1"
|
||||
"os"
|
||||
//"os/exec"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (ctx *appContext) loadStrftime() {
|
||||
ctx.datePattern = ctx.config.Section("email").Key("date_format").String()
|
||||
ctx.timePattern = `%H:%M`
|
||||
if val, _ := ctx.config.Section("email").Key("use_24h").Bool(); !val {
|
||||
ctx.timePattern = `%I:%M %p`
|
||||
func (app *appContext) loadStrftime() {
|
||||
app.datePattern = app.config.Section("email").Key("date_format").String()
|
||||
app.timePattern = `%H:%M`
|
||||
if val, _ := app.config.Section("email").Key("use_24h").Bool(); !val {
|
||||
app.timePattern = `%I:%M %p`
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (ctx *appContext) prettyTime(dt time.Time) (date, time string) {
|
||||
date, _ = strtime.Strftime(dt, ctx.datePattern)
|
||||
time, _ = strtime.Strftime(dt, ctx.timePattern)
|
||||
func (app *appContext) prettyTime(dt time.Time) (date, time string) {
|
||||
date, _ = strtime.Strftime(dt, app.datePattern)
|
||||
time, _ = strtime.Strftime(dt, app.timePattern)
|
||||
return
|
||||
}
|
||||
|
||||
func (ctx *appContext) formatDatetime(dt time.Time) string {
|
||||
d, t := ctx.prettyTime(dt)
|
||||
func (app *appContext) formatDatetime(dt time.Time) string {
|
||||
d, t := app.prettyTime(dt)
|
||||
return d + " " + t
|
||||
}
|
||||
|
||||
@@ -79,82 +80,86 @@ func timeDiff(a, b time.Time) (year, month, day, hour, min, sec int) {
|
||||
return
|
||||
}
|
||||
|
||||
func (ctx *appContext) checkInvites() {
|
||||
func (app *appContext) checkInvites() {
|
||||
current_time := time.Now()
|
||||
ctx.storage.loadInvites()
|
||||
app.storage.loadInvites()
|
||||
changed := false
|
||||
for code, data := range ctx.storage.invites {
|
||||
for code, data := range app.storage.invites {
|
||||
expiry := data.ValidTill
|
||||
if current_time.After(expiry) {
|
||||
ctx.debug.Printf("Housekeeping: Deleting old invite %s", code)
|
||||
app.debug.Printf("Housekeeping: Deleting old invite %s", code)
|
||||
notify := data.Notify
|
||||
if ctx.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 {
|
||||
ctx.debug.Printf("%s: Expiry notification", code)
|
||||
if app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 {
|
||||
app.debug.Printf("%s: Expiry notification", code)
|
||||
for address, settings := range notify {
|
||||
if settings["notify-expiry"] {
|
||||
if ctx.email.constructExpiry(code, data, ctx) != nil {
|
||||
ctx.err.Printf("%s: Failed to construct expiry notification", code)
|
||||
} else if ctx.email.send(address, ctx) != nil {
|
||||
ctx.err.Printf("%s: Failed to send expiry notification", code)
|
||||
} else {
|
||||
ctx.info.Printf("Sent expiry notification to %s", address)
|
||||
}
|
||||
go func() {
|
||||
if app.email.constructExpiry(code, data, app) != nil {
|
||||
app.err.Printf("%s: Failed to construct expiry notification", code)
|
||||
} else if app.email.send(address, app) != nil {
|
||||
app.err.Printf("%s: Failed to send expiry notification", code)
|
||||
} else {
|
||||
app.info.Printf("Sent expiry notification to %s", address)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
changed = true
|
||||
delete(ctx.storage.invites, code)
|
||||
delete(app.storage.invites, code)
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
ctx.storage.storeInvites()
|
||||
app.storage.storeInvites()
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *appContext) checkInvite(code string, used bool, username string) bool {
|
||||
func (app *appContext) checkInvite(code string, used bool, username string) bool {
|
||||
current_time := time.Now()
|
||||
ctx.storage.loadInvites()
|
||||
app.storage.loadInvites()
|
||||
changed := false
|
||||
if inv, match := ctx.storage.invites[code]; match {
|
||||
if inv, match := app.storage.invites[code]; match {
|
||||
expiry := inv.ValidTill
|
||||
if current_time.After(expiry) {
|
||||
ctx.debug.Printf("Housekeeping: Deleting old invite %s", code)
|
||||
app.debug.Printf("Housekeeping: Deleting old invite %s", code)
|
||||
notify := inv.Notify
|
||||
if ctx.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 {
|
||||
ctx.debug.Printf("%s: Expiry notification", code)
|
||||
if app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 {
|
||||
app.debug.Printf("%s: Expiry notification", code)
|
||||
for address, settings := range notify {
|
||||
if settings["notify-expiry"] {
|
||||
if ctx.email.constructExpiry(code, inv, ctx) != nil {
|
||||
ctx.err.Printf("%s: Failed to construct expiry notification", code)
|
||||
} else if ctx.email.send(address, ctx) != nil {
|
||||
ctx.err.Printf("%s: Failed to send expiry notification", code)
|
||||
} else {
|
||||
ctx.info.Printf("Sent expiry notification to %s", address)
|
||||
}
|
||||
go func() {
|
||||
if app.email.constructExpiry(code, inv, app) != nil {
|
||||
app.err.Printf("%s: Failed to construct expiry notification", code)
|
||||
} else if app.email.send(address, app) != nil {
|
||||
app.err.Printf("%s: Failed to send expiry notification", code)
|
||||
} else {
|
||||
app.info.Printf("Sent expiry notification to %s", address)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
changed = true
|
||||
match = false
|
||||
delete(ctx.storage.invites, code)
|
||||
delete(app.storage.invites, code)
|
||||
} else if used {
|
||||
changed = true
|
||||
del := false
|
||||
newInv := inv
|
||||
if newInv.RemainingUses == 1 {
|
||||
del = true
|
||||
delete(ctx.storage.invites, code)
|
||||
delete(app.storage.invites, code)
|
||||
} else if newInv.RemainingUses != 0 {
|
||||
// 0 means infinite i guess?
|
||||
newInv.RemainingUses -= 1
|
||||
}
|
||||
newInv.UsedBy = append(newInv.UsedBy, []string{username, ctx.formatDatetime(current_time)})
|
||||
newInv.UsedBy = append(newInv.UsedBy, []string{username, app.formatDatetime(current_time)})
|
||||
if !del {
|
||||
ctx.storage.invites[code] = newInv
|
||||
app.storage.invites[code] = newInv
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
ctx.storage.storeInvites()
|
||||
app.storage.storeInvites()
|
||||
}
|
||||
return match
|
||||
}
|
||||
@@ -170,17 +175,17 @@ type newUserReq struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
func (ctx *appContext) NewUser(gc *gin.Context) {
|
||||
func (app *appContext) NewUser(gc *gin.Context) {
|
||||
var req newUserReq
|
||||
gc.BindJSON(&req)
|
||||
ctx.debug.Printf("%s: New user attempt", req.Code)
|
||||
if !ctx.checkInvite(req.Code, false, "") {
|
||||
ctx.info.Printf("%s New user failed: invalid code", req.Code)
|
||||
app.debug.Printf("%s: New user attempt", req.Code)
|
||||
if !app.checkInvite(req.Code, false, "") {
|
||||
app.info.Printf("%s New user failed: invalid code", req.Code)
|
||||
gc.JSON(401, map[string]bool{"success": false})
|
||||
gc.Abort()
|
||||
return
|
||||
}
|
||||
validation := ctx.validator.validate(req.Password)
|
||||
validation := app.validator.validate(req.Password)
|
||||
valid := true
|
||||
for _, val := range validation {
|
||||
if !val {
|
||||
@@ -189,38 +194,40 @@ func (ctx *appContext) NewUser(gc *gin.Context) {
|
||||
}
|
||||
if !valid {
|
||||
// 200 bcs idk what i did in js
|
||||
ctx.info.Printf("%s New user failed: Invalid password", req.Code)
|
||||
app.info.Printf("%s New user failed: Invalid password", req.Code)
|
||||
gc.JSON(200, validation)
|
||||
gc.Abort()
|
||||
return
|
||||
}
|
||||
existingUser, _, _ := ctx.jf.userByName(req.Username, false)
|
||||
existingUser, _, _ := app.jf.userByName(req.Username, false)
|
||||
if existingUser != nil {
|
||||
msg := fmt.Sprintf("User already exists named %s", req.Username)
|
||||
ctx.info.Printf("%s New user failed: %s", req.Code, msg)
|
||||
app.info.Printf("%s New user failed: %s", req.Code, msg)
|
||||
respond(401, msg, gc)
|
||||
return
|
||||
}
|
||||
user, status, err := ctx.jf.newUser(req.Username, req.Password)
|
||||
user, status, err := app.jf.newUser(req.Username, req.Password)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
ctx.err.Printf("%s New user failed: Jellyfin responded with %d", req.Code, status)
|
||||
app.err.Printf("%s New user failed: Jellyfin responded with %d", req.Code, status)
|
||||
respond(401, "Unknown error", gc)
|
||||
return
|
||||
}
|
||||
ctx.checkInvite(req.Code, true, req.Username)
|
||||
invite := ctx.storage.invites[req.Code]
|
||||
if ctx.config.Section("notifications").Key("enabled").MustBool(false) {
|
||||
app.checkInvite(req.Code, true, req.Username)
|
||||
invite := app.storage.invites[req.Code]
|
||||
if app.config.Section("notifications").Key("enabled").MustBool(false) {
|
||||
for address, settings := range invite.Notify {
|
||||
if settings["notify-creation"] {
|
||||
if ctx.email.constructCreated(req.Code, req.Username, req.Email, invite, ctx) != nil {
|
||||
ctx.err.Printf("%s: Failed to construct user creation notification", req.Code)
|
||||
ctx.debug.Printf("%s: Error: %s", req.Code, err)
|
||||
} else if ctx.email.send(address, ctx) != nil {
|
||||
ctx.err.Printf("%s: Failed to send user creation notification", req.Code)
|
||||
ctx.debug.Printf("%s: Error: %s", req.Code, err)
|
||||
} else {
|
||||
ctx.info.Printf("%s: Sent user creation notification to %s", req.Code, address)
|
||||
}
|
||||
go func() {
|
||||
if app.email.constructCreated(req.Code, req.Username, req.Email, invite, app) != nil {
|
||||
app.err.Printf("%s: Failed to construct user creation notification", req.Code)
|
||||
app.debug.Printf("%s: Error: %s", req.Code, err)
|
||||
} else if app.email.send(address, app) != nil {
|
||||
app.err.Printf("%s: Failed to send user creation notification", req.Code)
|
||||
app.debug.Printf("%s: Error: %s", req.Code, err)
|
||||
} else {
|
||||
app.info.Printf("%s: Sent user creation notification to %s", req.Code, address)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -228,23 +235,23 @@ func (ctx *appContext) NewUser(gc *gin.Context) {
|
||||
if user["Id"] != nil {
|
||||
id = user["Id"].(string)
|
||||
}
|
||||
if len(ctx.storage.policy) != 0 {
|
||||
status, err = ctx.jf.setPolicy(id, ctx.storage.policy)
|
||||
if len(app.storage.policy) != 0 {
|
||||
status, err = app.jf.setPolicy(id, app.storage.policy)
|
||||
if !(status == 200 || status == 204) {
|
||||
ctx.err.Printf("%s: Failed to set user policy: Code %d", req.Code, status)
|
||||
app.err.Printf("%s: Failed to set user policy: Code %d", req.Code, status)
|
||||
}
|
||||
}
|
||||
if len(ctx.storage.configuration) != 0 && len(ctx.storage.displayprefs) != 0 {
|
||||
status, err = ctx.jf.setConfiguration(id, ctx.storage.configuration)
|
||||
if len(app.storage.configuration) != 0 && len(app.storage.displayprefs) != 0 {
|
||||
status, err = app.jf.setConfiguration(id, app.storage.configuration)
|
||||
if (status == 200 || status == 204) && err == nil {
|
||||
status, err = ctx.jf.setDisplayPreferences(id, ctx.storage.displayprefs)
|
||||
status, err = app.jf.setDisplayPreferences(id, app.storage.displayprefs)
|
||||
} else {
|
||||
ctx.err.Printf("%s: Failed to set configuration template: Code %d", req.Code, status)
|
||||
app.err.Printf("%s: Failed to set configuration template: Code %d", req.Code, status)
|
||||
}
|
||||
}
|
||||
if ctx.config.Section("password_resets").Key("enabled").MustBool(false) {
|
||||
ctx.storage.emails[id] = req.Email
|
||||
ctx.storage.storeEmails()
|
||||
if app.config.Section("password_resets").Key("enabled").MustBool(false) {
|
||||
app.storage.emails[id] = req.Email
|
||||
app.storage.storeEmails()
|
||||
}
|
||||
gc.JSON(200, validation)
|
||||
}
|
||||
@@ -259,10 +266,10 @@ type generateInviteReq struct {
|
||||
RemainingUses int `json:"remaining-uses"`
|
||||
}
|
||||
|
||||
func (ctx *appContext) GenerateInvite(gc *gin.Context) {
|
||||
func (app *appContext) GenerateInvite(gc *gin.Context) {
|
||||
var req generateInviteReq
|
||||
ctx.debug.Println("Generating new invite")
|
||||
ctx.storage.loadInvites()
|
||||
app.debug.Println("Generating new invite")
|
||||
app.storage.loadInvites()
|
||||
gc.BindJSON(&req)
|
||||
current_time := time.Now()
|
||||
valid_till := current_time.AddDate(0, 0, req.Days)
|
||||
@@ -280,40 +287,40 @@ func (ctx *appContext) GenerateInvite(gc *gin.Context) {
|
||||
invite.RemainingUses = 1
|
||||
}
|
||||
invite.ValidTill = valid_till
|
||||
if req.Email != "" && ctx.config.Section("invite_emails").Key("enabled").MustBool(false) {
|
||||
ctx.debug.Printf("%s: Sending invite email", invite_code)
|
||||
if req.Email != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
|
||||
app.debug.Printf("%s: Sending invite email", invite_code)
|
||||
invite.Email = req.Email
|
||||
if err := ctx.email.constructInvite(invite_code, invite, ctx); err != nil {
|
||||
if err := app.email.constructInvite(invite_code, invite, app); err != nil {
|
||||
invite.Email = fmt.Sprintf("Failed to send to %s", req.Email)
|
||||
ctx.err.Printf("%s: Failed to construct invite email", invite_code)
|
||||
ctx.debug.Printf("%s: Error: %s", invite_code, err)
|
||||
} else if err := ctx.email.send(req.Email, ctx); err != nil {
|
||||
app.err.Printf("%s: Failed to construct invite email", invite_code)
|
||||
app.debug.Printf("%s: Error: %s", invite_code, err)
|
||||
} else if err := app.email.send(req.Email, app); err != nil {
|
||||
invite.Email = fmt.Sprintf("Failed to send to %s", req.Email)
|
||||
ctx.err.Printf("%s: %s", invite_code, invite.Email)
|
||||
ctx.debug.Printf("%s: Error: %s", invite_code, err)
|
||||
app.err.Printf("%s: %s", invite_code, invite.Email)
|
||||
app.debug.Printf("%s: Error: %s", invite_code, err)
|
||||
} else {
|
||||
ctx.info.Printf("%s: Sent invite email to %s", invite_code, req.Email)
|
||||
app.info.Printf("%s: Sent invite email to %s", invite_code, req.Email)
|
||||
}
|
||||
}
|
||||
ctx.storage.invites[invite_code] = invite
|
||||
ctx.storage.storeInvites()
|
||||
app.storage.invites[invite_code] = invite
|
||||
app.storage.storeInvites()
|
||||
gc.JSON(200, map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
func (ctx *appContext) GetInvites(gc *gin.Context) {
|
||||
ctx.debug.Println("Invites requested")
|
||||
func (app *appContext) GetInvites(gc *gin.Context) {
|
||||
app.debug.Println("Invites requested")
|
||||
current_time := time.Now()
|
||||
ctx.storage.loadInvites()
|
||||
ctx.checkInvites()
|
||||
app.storage.loadInvites()
|
||||
app.checkInvites()
|
||||
var invites []map[string]interface{}
|
||||
for code, inv := range ctx.storage.invites {
|
||||
for code, inv := range app.storage.invites {
|
||||
_, _, days, hours, minutes, _ := timeDiff(inv.ValidTill, current_time)
|
||||
invite := make(map[string]interface{})
|
||||
invite["code"] = code
|
||||
invite["days"] = days
|
||||
invite["hours"] = hours
|
||||
invite["minutes"] = minutes
|
||||
invite["created"] = ctx.formatDatetime(inv.Created)
|
||||
invite["created"] = app.formatDatetime(inv.Created)
|
||||
if len(inv.UsedBy) != 0 {
|
||||
invite["used-by"] = inv.UsedBy
|
||||
}
|
||||
@@ -329,11 +336,11 @@ func (ctx *appContext) GetInvites(gc *gin.Context) {
|
||||
}
|
||||
if len(inv.Notify) != 0 {
|
||||
var address string
|
||||
if ctx.config.Section("ui").Key("jellyfin_login").MustBool(false) {
|
||||
ctx.storage.loadEmails()
|
||||
address = ctx.storage.emails[gc.GetString("jfId")].(string)
|
||||
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
|
||||
app.storage.loadEmails()
|
||||
address = app.storage.emails[gc.GetString("jfId")].(string)
|
||||
} else {
|
||||
address = ctx.config.Section("ui").Key("email").String()
|
||||
address = app.config.Section("ui").Key("email").String()
|
||||
}
|
||||
if _, ok := inv.Notify[address]; ok {
|
||||
for _, notify_type := range []string{"notify-expiry", "notify-creation"} {
|
||||
@@ -356,34 +363,34 @@ type notifySetting struct {
|
||||
NotifyCreation bool `json:"notify-creation"`
|
||||
}
|
||||
|
||||
func (ctx *appContext) SetNotify(gc *gin.Context) {
|
||||
func (app *appContext) SetNotify(gc *gin.Context) {
|
||||
var req map[string]notifySetting
|
||||
gc.BindJSON(&req)
|
||||
changed := false
|
||||
for code, settings := range req {
|
||||
ctx.debug.Printf("%s: Notification settings change requested", code)
|
||||
ctx.storage.loadInvites()
|
||||
ctx.storage.loadEmails()
|
||||
invite, ok := ctx.storage.invites[code]
|
||||
app.debug.Printf("%s: Notification settings change requested", code)
|
||||
app.storage.loadInvites()
|
||||
app.storage.loadEmails()
|
||||
invite, ok := app.storage.invites[code]
|
||||
if !ok {
|
||||
ctx.err.Printf("%s Notification setting change failed: Invalid code", code)
|
||||
app.err.Printf("%s Notification setting change failed: Invalid code", code)
|
||||
gc.JSON(400, map[string]string{"error": "Invalid invite code"})
|
||||
gc.Abort()
|
||||
return
|
||||
}
|
||||
var address string
|
||||
if ctx.config.Section("ui").Key("jellyfin_login").MustBool(false) {
|
||||
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
|
||||
var ok bool
|
||||
address, ok = ctx.storage.emails[gc.GetString("jfId")].(string)
|
||||
address, ok = app.storage.emails[gc.GetString("jfId")].(string)
|
||||
if !ok {
|
||||
ctx.err.Printf("%s: Couldn't find email address. Make sure it's set", code)
|
||||
ctx.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId"))
|
||||
app.err.Printf("%s: Couldn't find email address. Make sure it's set", code)
|
||||
app.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId"))
|
||||
gc.JSON(500, map[string]string{"error": "Missing user email"})
|
||||
gc.Abort()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
address = ctx.config.Section("ui").Key("email").String()
|
||||
address = app.config.Section("ui").Key("email").String()
|
||||
}
|
||||
if invite.Notify == nil {
|
||||
invite.Notify = map[string]map[string]bool{}
|
||||
@@ -395,20 +402,20 @@ func (ctx *appContext) SetNotify(gc *gin.Context) {
|
||||
*/
|
||||
if invite.Notify[address]["notify-expiry"] != settings.NotifyExpiry {
|
||||
invite.Notify[address]["notify-expiry"] = settings.NotifyExpiry
|
||||
ctx.debug.Printf("%s: Set \"notify-expiry\" to %t for %s", code, settings.NotifyExpiry, address)
|
||||
app.debug.Printf("%s: Set \"notify-expiry\" to %t for %s", code, settings.NotifyExpiry, address)
|
||||
changed = true
|
||||
}
|
||||
if invite.Notify[address]["notify-creation"] != settings.NotifyCreation {
|
||||
invite.Notify[address]["notify-creation"] = settings.NotifyCreation
|
||||
ctx.debug.Printf("%s: Set \"notify-creation\" to %t for %s", code, settings.NotifyExpiry, address)
|
||||
app.debug.Printf("%s: Set \"notify-creation\" to %t for %s", code, settings.NotifyExpiry, address)
|
||||
changed = true
|
||||
}
|
||||
if changed {
|
||||
ctx.storage.invites[code] = invite
|
||||
app.storage.invites[code] = invite
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
ctx.storage.storeInvites()
|
||||
app.storage.storeInvites()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,20 +423,20 @@ type deleteReq struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
func (ctx *appContext) DeleteInvite(gc *gin.Context) {
|
||||
func (app *appContext) DeleteInvite(gc *gin.Context) {
|
||||
var req deleteReq
|
||||
gc.BindJSON(&req)
|
||||
ctx.debug.Printf("%s: Deletion requested", req.Code)
|
||||
app.debug.Printf("%s: Deletion requested", req.Code)
|
||||
var ok bool
|
||||
_, ok = ctx.storage.invites[req.Code]
|
||||
_, ok = app.storage.invites[req.Code]
|
||||
if ok {
|
||||
delete(ctx.storage.invites, req.Code)
|
||||
ctx.storage.storeInvites()
|
||||
ctx.info.Printf("%s: Invite deleted", req.Code)
|
||||
delete(app.storage.invites, req.Code)
|
||||
app.storage.storeInvites()
|
||||
app.info.Printf("%s: Invite deleted", req.Code)
|
||||
gc.JSON(200, map[string]bool{"success": true})
|
||||
return
|
||||
}
|
||||
ctx.err.Printf("%s: Deletion failed: Invalid code", req.Code)
|
||||
app.err.Printf("%s: Deletion failed: Invalid code", req.Code)
|
||||
respond(401, "Code doesn't exist", gc)
|
||||
}
|
||||
|
||||
@@ -442,21 +449,21 @@ type respUser struct {
|
||||
Email string `json:"email,omitempty"`
|
||||
}
|
||||
|
||||
func (ctx *appContext) GetUsers(gc *gin.Context) {
|
||||
ctx.debug.Println("Users requested")
|
||||
func (app *appContext) GetUsers(gc *gin.Context) {
|
||||
app.debug.Println("Users requested")
|
||||
var resp userResp
|
||||
resp.UserList = []respUser{}
|
||||
users, status, err := ctx.jf.getUsers(false)
|
||||
users, status, err := app.jf.getUsers(false)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
ctx.err.Printf("Failed to get users from Jellyfin: Code %d", status)
|
||||
ctx.debug.Printf("Error: %s", err)
|
||||
app.err.Printf("Failed to get users from Jellyfin: Code %d", status)
|
||||
app.debug.Printf("Error: %s", err)
|
||||
respond(500, "Couldn't get users", gc)
|
||||
return
|
||||
}
|
||||
for _, jfUser := range users {
|
||||
var user respUser
|
||||
user.Name = jfUser["Name"].(string)
|
||||
if email, ok := ctx.storage.emails[jfUser["Id"].(string)]; ok {
|
||||
if email, ok := app.storage.emails[jfUser["Id"].(string)]; ok {
|
||||
user.Email = email.(string)
|
||||
}
|
||||
resp.UserList = append(resp.UserList, user)
|
||||
@@ -464,24 +471,24 @@ func (ctx *appContext) GetUsers(gc *gin.Context) {
|
||||
gc.JSON(200, resp)
|
||||
}
|
||||
|
||||
func (ctx *appContext) ModifyEmails(gc *gin.Context) {
|
||||
func (app *appContext) ModifyEmails(gc *gin.Context) {
|
||||
var req map[string]string
|
||||
gc.BindJSON(&req)
|
||||
ctx.debug.Println("Email modification requested")
|
||||
users, status, err := ctx.jf.getUsers(false)
|
||||
app.debug.Println("Email modification requested")
|
||||
users, status, err := app.jf.getUsers(false)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
ctx.err.Printf("Failed to get users from Jellyfin: Code %d", status)
|
||||
ctx.debug.Printf("Error: %s", err)
|
||||
app.err.Printf("Failed to get users from Jellyfin: Code %d", status)
|
||||
app.debug.Printf("Error: %s", err)
|
||||
respond(500, "Couldn't get users", gc)
|
||||
return
|
||||
}
|
||||
for _, jfUser := range users {
|
||||
if address, ok := req[jfUser["Name"].(string)]; ok {
|
||||
ctx.storage.emails[jfUser["Id"].(string)] = address
|
||||
app.storage.emails[jfUser["Id"].(string)] = address
|
||||
}
|
||||
}
|
||||
ctx.storage.storeEmails()
|
||||
ctx.info.Println("Email list modified")
|
||||
app.storage.storeEmails()
|
||||
app.info.Println("Email list modified")
|
||||
gc.JSON(200, map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
@@ -490,46 +497,46 @@ type defaultsReq struct {
|
||||
Homescreen bool `json:"homescreen"`
|
||||
}
|
||||
|
||||
func (ctx *appContext) SetDefaults(gc *gin.Context) {
|
||||
func (app *appContext) SetDefaults(gc *gin.Context) {
|
||||
var req defaultsReq
|
||||
gc.BindJSON(&req)
|
||||
ctx.info.Printf("Getting user defaults from \"%s\"", req.Username)
|
||||
user, status, err := ctx.jf.userByName(req.Username, false)
|
||||
app.info.Printf("Getting user defaults from \"%s\"", req.Username)
|
||||
user, status, err := app.jf.userByName(req.Username, false)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
ctx.err.Printf("Failed to get user from Jellyfin: Code %d", status)
|
||||
ctx.debug.Printf("Error: %s", err)
|
||||
app.err.Printf("Failed to get user from Jellyfin: Code %d", status)
|
||||
app.debug.Printf("Error: %s", err)
|
||||
respond(500, "Couldn't get user", gc)
|
||||
return
|
||||
}
|
||||
userId := user["Id"].(string)
|
||||
policy := user["Policy"].(map[string]interface{})
|
||||
ctx.storage.policy = policy
|
||||
ctx.storage.storePolicy()
|
||||
ctx.debug.Println("User policy template stored")
|
||||
app.storage.policy = policy
|
||||
app.storage.storePolicy()
|
||||
app.debug.Println("User policy template stored")
|
||||
if req.Homescreen {
|
||||
configuration := user["Configuration"].(map[string]interface{})
|
||||
var displayprefs map[string]interface{}
|
||||
displayprefs, status, err = ctx.jf.getDisplayPreferences(userId)
|
||||
displayprefs, status, err = app.jf.getDisplayPreferences(userId)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
ctx.err.Printf("Failed to get DisplayPrefs: Code %d", status)
|
||||
ctx.debug.Printf("Error: %s", err)
|
||||
app.err.Printf("Failed to get DisplayPrefs: Code %d", status)
|
||||
app.debug.Printf("Error: %s", err)
|
||||
respond(500, "Couldn't get displayprefs", gc)
|
||||
return
|
||||
}
|
||||
ctx.storage.configuration = configuration
|
||||
ctx.storage.displayprefs = displayprefs
|
||||
ctx.storage.storeConfiguration()
|
||||
ctx.debug.Println("Configuration template stored")
|
||||
ctx.storage.storeDisplayprefs()
|
||||
ctx.debug.Println("DisplayPrefs template stored")
|
||||
app.storage.configuration = configuration
|
||||
app.storage.displayprefs = displayprefs
|
||||
app.storage.storeConfiguration()
|
||||
app.debug.Println("Configuration template stored")
|
||||
app.storage.storeDisplayprefs()
|
||||
app.debug.Println("DisplayPrefs template stored")
|
||||
}
|
||||
gc.JSON(200, map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
func (ctx *appContext) GetConfig(gc *gin.Context) {
|
||||
ctx.info.Println("Config requested")
|
||||
func (app *appContext) GetConfig(gc *gin.Context) {
|
||||
app.info.Println("Config requested")
|
||||
resp := map[string]interface{}{}
|
||||
for section, settings := range ctx.configBase {
|
||||
for section, settings := range app.configBase {
|
||||
if section == "order" {
|
||||
resp[section] = settings.([]interface{})
|
||||
} else {
|
||||
@@ -541,7 +548,7 @@ func (ctx *appContext) GetConfig(gc *gin.Context) {
|
||||
resp[section].(map[string]interface{})[key] = values.(map[string]interface{})
|
||||
if key != "meta" {
|
||||
dataType := resp[section].(map[string]interface{})[key].(map[string]interface{})["type"].(string)
|
||||
configKey := ctx.config.Section(section).Key(key)
|
||||
configKey := app.config.Section(section).Key(key)
|
||||
if dataType == "number" {
|
||||
if val, err := configKey.Int(); err == nil {
|
||||
resp[section].(map[string]interface{})[key].(map[string]interface{})["value"] = val
|
||||
@@ -559,11 +566,11 @@ func (ctx *appContext) GetConfig(gc *gin.Context) {
|
||||
gc.JSON(200, resp)
|
||||
}
|
||||
|
||||
func (ctx *appContext) ModifyConfig(gc *gin.Context) {
|
||||
ctx.info.Println("Config modification requested")
|
||||
func (app *appContext) ModifyConfig(gc *gin.Context) {
|
||||
app.info.Println("Config modification requested")
|
||||
var req map[string]interface{}
|
||||
gc.BindJSON(&req)
|
||||
tempConfig, _ := ini.Load(ctx.config_path)
|
||||
tempConfig, _ := ini.Load(app.config_path)
|
||||
for section, settings := range req {
|
||||
_, err := tempConfig.GetSection(section)
|
||||
if section != "restart-program" && err == nil {
|
||||
@@ -572,17 +579,46 @@ func (ctx *appContext) ModifyConfig(gc *gin.Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
tempConfig.SaveTo(ctx.config_path)
|
||||
ctx.debug.Println("Config saved")
|
||||
tempConfig.SaveTo(app.config_path)
|
||||
app.debug.Println("Config saved")
|
||||
gc.JSON(200, map[string]bool{"success": true})
|
||||
if req["restart-program"].(bool) {
|
||||
ctx.info.Println("Restarting...")
|
||||
err := Restart()
|
||||
app.info.Println("Restarting...")
|
||||
err := app.Restart()
|
||||
if err != nil {
|
||||
ctx.err.Printf("Couldn't restart, try restarting manually. (%s)", err)
|
||||
app.err.Printf("Couldn't restart, try restarting manually. (%s)", err)
|
||||
}
|
||||
}
|
||||
ctx.loadConfig()
|
||||
app.loadConfig()
|
||||
// Reinitialize password validator on config change, as opposed to every applicable request like in python.
|
||||
if _, ok := req["password_validation"]; ok {
|
||||
app.debug.Println("Reinitializing validator")
|
||||
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),
|
||||
}
|
||||
if !app.config.Section("password_validation").Key("enabled").MustBool(false) {
|
||||
for key := range validatorConf {
|
||||
validatorConf[key] = 0
|
||||
}
|
||||
}
|
||||
app.validator.init(validatorConf)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *appContext) Logout(gc *gin.Context) {
|
||||
cookie, err := gc.Cookie("refresh")
|
||||
if err != nil {
|
||||
app.debug.Printf("Couldn't get cookies: %s", err)
|
||||
respond(500, "Couldn't fetch cookies", gc)
|
||||
return
|
||||
}
|
||||
app.invalidTokens = append(app.invalidTokens, cookie)
|
||||
gc.SetCookie("refresh", "invalid", -1, "/", gc.Request.URL.Hostname(), true, true)
|
||||
gc.JSON(200, map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
// func Restart() error {
|
||||
@@ -611,10 +647,11 @@ func (ctx *appContext) ModifyConfig(gc *gin.Context) {
|
||||
// panic(fmt.Errorf("restarting"))
|
||||
// }
|
||||
|
||||
func Restart() error {
|
||||
func (app *appContext) Restart() error {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
os.Exit(0)
|
||||
signal.Notify(app.quit, os.Interrupt)
|
||||
<-app.quit
|
||||
}
|
||||
}()
|
||||
args := os.Args
|
||||
@@ -625,7 +662,6 @@ func Restart() error {
|
||||
os.Setenv("JFA_EXEC", args[0])
|
||||
}
|
||||
env := os.Environ()
|
||||
fmt.Printf("EXECUTABLE: %s\n", os.Getenv("JFA_EXEC"))
|
||||
err := syscall.Exec(os.Getenv("JFA_EXEC"), []string{""}, env)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
339
auth.go
@@ -3,147 +3,244 @@ package main
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (ctx *appContext) webAuth() gin.HandlerFunc {
|
||||
return ctx.authenticate
|
||||
func (app *appContext) webAuth() gin.HandlerFunc {
|
||||
return app.authenticate
|
||||
}
|
||||
|
||||
func (ctx *appContext) authenticate(gc *gin.Context) {
|
||||
header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)
|
||||
if header[0] != "Basic" {
|
||||
ctx.debug.Println("Invalid authentication header")
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
auth, _ := base64.StdEncoding.DecodeString(header[1])
|
||||
creds := strings.SplitN(string(auth), ":", 2)
|
||||
token, err := jwt.Parse(creds[0], func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
ctx.debug.Printf("Invalid JWT signing method %s", token.Header["alg"])
|
||||
return nil, fmt.Errorf("Unexpected signing method %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(os.Getenv("JFA_SECRET")), nil
|
||||
})
|
||||
if err != nil {
|
||||
ctx.debug.Printf("Auth denied: %s", err)
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
var userId string
|
||||
var jfId string
|
||||
if ok && token.Valid {
|
||||
userId = claims["id"].(string)
|
||||
jfId = claims["jfid"].(string)
|
||||
} else {
|
||||
ctx.debug.Printf("Invalid token")
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
match := false
|
||||
for _, user := range ctx.users {
|
||||
if user.UserID == userId {
|
||||
match = true
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
ctx.debug.Printf("Couldn't find user ID %s", userId)
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
gc.Set("jfId", jfId)
|
||||
gc.Set("userId", userId)
|
||||
ctx.debug.Println("Authentication successful")
|
||||
gc.Next()
|
||||
}
|
||||
|
||||
func (ctx *appContext) GetToken(gc *gin.Context) {
|
||||
ctx.info.Println("Token requested (login attempt)")
|
||||
header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)
|
||||
if header[0] != "Basic" {
|
||||
ctx.debug.Println("Invalid authentication header")
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
auth, _ := base64.StdEncoding.DecodeString(header[1])
|
||||
creds := strings.SplitN(string(auth), ":", 2)
|
||||
match := false
|
||||
var userId string
|
||||
for _, user := range ctx.users {
|
||||
if user.Username == creds[0] && user.Password == creds[1] {
|
||||
match = true
|
||||
userId = user.UserID
|
||||
}
|
||||
}
|
||||
jfId := ""
|
||||
if !match {
|
||||
if !ctx.jellyfinLogin {
|
||||
ctx.info.Println("Auth failed: Invalid username and/or password")
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
var status int
|
||||
var err error
|
||||
var user map[string]interface{}
|
||||
user, status, err = ctx.authJf.authenticate(creds[0], creds[1])
|
||||
jfId = user["Id"].(string)
|
||||
if status != 200 || err != nil {
|
||||
if status == 401 {
|
||||
ctx.info.Println("Auth failed: Invalid username and/or password")
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
ctx.err.Printf("Auth failed: Couldn't authenticate with Jellyfin: Code %d", status)
|
||||
respond(500, "Jellyfin error", gc)
|
||||
return
|
||||
} else {
|
||||
if ctx.config.Section("ui").Key("admin_only").MustBool(true) {
|
||||
if !user["Policy"].(map[string]interface{})["IsAdministrator"].(bool) {
|
||||
ctx.debug.Printf("Auth failed: User \"%s\" isn't admin", creds[0])
|
||||
respond(401, "Unauthorized", gc)
|
||||
}
|
||||
}
|
||||
newuser := User{}
|
||||
newuser.UserID = shortuuid.New()
|
||||
userId = newuser.UserID
|
||||
// uuid, nothing else identifiable!
|
||||
ctx.debug.Printf("Token generated for user \"%s\"", creds[0])
|
||||
ctx.users = append(ctx.users, newuser)
|
||||
}
|
||||
}
|
||||
token, err := CreateToken(userId, jfId)
|
||||
if err != nil {
|
||||
respond(500, "Error generating token", gc)
|
||||
}
|
||||
resp := map[string]string{"token": token}
|
||||
gc.JSON(200, resp)
|
||||
}
|
||||
|
||||
func CreateToken(userId string, jfId string) (string, error) {
|
||||
// CreateToken returns a web token as well as a refresh token, which can be used to obtain new tokens.
|
||||
func CreateToken(userId, jfId string) (string, string, error) {
|
||||
var token, refresh string
|
||||
claims := jwt.MapClaims{
|
||||
"valid": true,
|
||||
"id": userId,
|
||||
"exp": time.Now().Add(time.Minute * 20).Unix(),
|
||||
"exp": strconv.FormatInt(time.Now().Add(time.Minute*20).Unix(), 10),
|
||||
"jfid": jfId,
|
||||
"type": "bearer",
|
||||
}
|
||||
|
||||
tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
token, err := tk.SignedString([]byte(os.Getenv("JFA_SECRET")))
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", "", err
|
||||
}
|
||||
return token, nil
|
||||
claims["exp"] = strconv.FormatInt(time.Now().Add(time.Hour*24).Unix(), 10)
|
||||
claims["type"] = "refresh"
|
||||
tk = jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
refresh, err = tk.SignedString([]byte(os.Getenv("JFA_SECRET")))
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return token, refresh, nil
|
||||
}
|
||||
|
||||
func respond(code int, message string, gc *gin.Context) {
|
||||
resp := map[string]string{"error": message}
|
||||
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")
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
auth, _ := base64.StdEncoding.DecodeString(header[1])
|
||||
creds := strings.SplitN(string(auth), ":", 2)
|
||||
token, err := jwt.Parse(creds[0], checkToken)
|
||||
if err != nil {
|
||||
app.debug.Printf("Auth denied: %s", err)
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
expiryUnix, err := strconv.ParseInt(claims["exp"].(string), 10, 64)
|
||||
if err != nil {
|
||||
app.debug.Printf("Auth denied: %s", err)
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
expiry := time.Unix(expiryUnix, 0)
|
||||
if !(ok && token.Valid && claims["type"].(string) == "bearer" && expiry.After(time.Now())) {
|
||||
app.debug.Printf("Auth denied: Invalid token")
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
userID := claims["id"].(string)
|
||||
jfID := claims["jfid"].(string)
|
||||
match := false
|
||||
for _, user := range app.users {
|
||||
if user.UserID == userID {
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
app.debug.Printf("Couldn't find user ID \"%s\"", userID)
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
gc.Set("jfId", jfID)
|
||||
gc.Set("userId", userID)
|
||||
app.debug.Println("Auth succeeded")
|
||||
gc.Next()
|
||||
}
|
||||
|
||||
func checkToken(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("Unexpected signing method %v", token.Header["alg"])
|
||||
}
|
||||
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) {
|
||||
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)
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
65
config.go
@@ -1,46 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"gopkg.in/ini.v1"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
func (ctx *appContext) loadConfig() error {
|
||||
/*var DeCamel ini.NameMapper = func(raw string) string {
|
||||
out := make([]rune, 0, len(raw))
|
||||
upper := 0
|
||||
for _, c := range raw {
|
||||
if unicode.IsUpper(c) {
|
||||
upper++
|
||||
}
|
||||
if upper == 2 {
|
||||
out = append(out, '_')
|
||||
upper = 0
|
||||
}
|
||||
out = append(out, unicode.ToLower(c))
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func (app *appContext) loadDefaults() (err error) {
|
||||
var cfb []byte
|
||||
cfb, err = ioutil.ReadFile(app.configBase_path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
json.Unmarshal(cfb, app.defaults)
|
||||
return
|
||||
}*/
|
||||
|
||||
func (app *appContext) loadConfig() error {
|
||||
var err error
|
||||
ctx.config, err = ini.Load(ctx.config_path)
|
||||
app.config, err = ini.Load(app.config_path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx.config.Section("jellyfin").Key("public_server").SetValue(ctx.config.Section("jellyfin").Key("public_server").MustString(ctx.config.Section("jellyfin").Key("server").String()))
|
||||
app.config.Section("jellyfin").Key("public_server").SetValue(app.config.Section("jellyfin").Key("public_server").MustString(app.config.Section("jellyfin").Key("server").String()))
|
||||
|
||||
for _, key := range ctx.config.Section("files").Keys() {
|
||||
for _, key := range app.config.Section("files").Keys() {
|
||||
// if key.MustString("") == "" && key.Name() != "custom_css" {
|
||||
// key.SetValue(filepath.Join(ctx.data_path, (key.Name() + ".json")))
|
||||
// key.SetValue(filepath.Join(app.data_path, (key.Name() + ".json")))
|
||||
// }
|
||||
key.SetValue(key.MustString(filepath.Join(ctx.data_path, (key.Name() + ".json"))))
|
||||
key.SetValue(key.MustString(filepath.Join(app.data_path, (key.Name() + ".json"))))
|
||||
}
|
||||
for _, key := range []string{"user_configuration", "user_displayprefs"} {
|
||||
// if ctx.config.Section("files").Key(key).MustString("") == "" {
|
||||
// key.SetValue(filepath.Join(ctx.data_path, (key.Name() + ".json")))
|
||||
// if app.config.Section("files").Key(key).MustString("") == "" {
|
||||
// key.SetValue(filepath.Join(app.data_path, (key.Name() + ".json")))
|
||||
// }
|
||||
ctx.config.Section("files").Key(key).SetValue(ctx.config.Section("files").Key(key).MustString(filepath.Join(ctx.data_path, (key + ".json"))))
|
||||
app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.data_path, (key + ".json"))))
|
||||
}
|
||||
|
||||
ctx.config.Section("email").Key("no_username").SetValue(strconv.FormatBool(ctx.config.Section("email").Key("no_username").MustBool(false)))
|
||||
app.config.Section("email").Key("no_username").SetValue(strconv.FormatBool(app.config.Section("email").Key("no_username").MustBool(false)))
|
||||
|
||||
ctx.config.Section("password_resets").Key("email_html").SetValue(ctx.config.Section("password_resets").Key("email_html").MustString(filepath.Join(ctx.local_path, "email.html")))
|
||||
ctx.config.Section("password_resets").Key("email_text").SetValue(ctx.config.Section("password_resets").Key("email_text").MustString(filepath.Join(ctx.local_path, "email.txt")))
|
||||
app.config.Section("password_resets").Key("email_html").SetValue(app.config.Section("password_resets").Key("email_html").MustString(filepath.Join(app.local_path, "email.html")))
|
||||
app.config.Section("password_resets").Key("email_text").SetValue(app.config.Section("password_resets").Key("email_text").MustString(filepath.Join(app.local_path, "email.txt")))
|
||||
|
||||
ctx.config.Section("invite_emails").Key("email_html").SetValue(ctx.config.Section("invite_emails").Key("email_html").MustString(filepath.Join(ctx.local_path, "invite-email.html")))
|
||||
ctx.config.Section("invite_emails").Key("email_text").SetValue(ctx.config.Section("invite_emails").Key("email_text").MustString(filepath.Join(ctx.local_path, "invite-email.txt")))
|
||||
app.config.Section("invite_emails").Key("email_html").SetValue(app.config.Section("invite_emails").Key("email_html").MustString(filepath.Join(app.local_path, "invite-email.html")))
|
||||
app.config.Section("invite_emails").Key("email_text").SetValue(app.config.Section("invite_emails").Key("email_text").MustString(filepath.Join(app.local_path, "invite-email.txt")))
|
||||
|
||||
ctx.config.Section("notifications").Key("expiry_html").SetValue(ctx.config.Section("notifications").Key("expiry_html").MustString(filepath.Join(ctx.local_path, "expired.html")))
|
||||
ctx.config.Section("notifications").Key("expiry_text").SetValue(ctx.config.Section("notifications").Key("expiry_text").MustString(filepath.Join(ctx.local_path, "expired.txt")))
|
||||
app.config.Section("notifications").Key("expiry_html").SetValue(app.config.Section("notifications").Key("expiry_html").MustString(filepath.Join(app.local_path, "expired.html")))
|
||||
app.config.Section("notifications").Key("expiry_text").SetValue(app.config.Section("notifications").Key("expiry_text").MustString(filepath.Join(app.local_path, "expired.txt")))
|
||||
|
||||
ctx.config.Section("notifications").Key("created_html").SetValue(ctx.config.Section("notifications").Key("created_html").MustString(filepath.Join(ctx.local_path, "created.html")))
|
||||
ctx.config.Section("notifications").Key("created_text").SetValue(ctx.config.Section("notifications").Key("created_text").MustString(filepath.Join(ctx.local_path, "created.txt")))
|
||||
app.config.Section("notifications").Key("created_html").SetValue(app.config.Section("notifications").Key("created_html").MustString(filepath.Join(app.local_path, "created.html")))
|
||||
app.config.Section("notifications").Key("created_text").SetValue(app.config.Section("notifications").Key("created_text").MustString(filepath.Join(app.local_path, "created.txt")))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
## fixconfig
|
||||
### fixconfig
|
||||
|
||||
Python's `json` library retains the order of data in a JSON file, which meant settings sent to the web page would be in the right order. Go's `encoding/json` and maps do not retain order, so this script opens the json file, and for each section, adds an "order" list which tells the web page in which order to display settings.
|
||||
|
||||
Place the config base at `./config-base.json`, run `python fixconfig.py`, and the new config base will be stored at `./ordered-config-base.json`.
|
||||
Specify the input and output files with `-i` and `-o` respectively.
|
||||
|
||||
### jsontostruct
|
||||
|
||||
Generates a go struct from `config-base.json`. I wrote this because i was annoyed with the `ini` library, but i've since realised mapping the ini values onto it is painful.
|
||||
|
||||
|
||||
|
||||
@@ -147,7 +147,8 @@
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": false
|
||||
"value": false,
|
||||
"description": "Enables debug logging and exposes pprof as a route (Don't use in production!)"
|
||||
},
|
||||
"contact_message": {
|
||||
"name": "Contact message",
|
||||
|
||||
541
config/configStruct.go
Normal file
@@ -0,0 +1,541 @@
|
||||
package main
|
||||
|
||||
type Metadata struct{
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type Config struct{
|
||||
Order []string `json:"order"`
|
||||
Jellyfin struct{
|
||||
Order []string `json:"order"`
|
||||
Meta Metadata `json:"meta"`
|
||||
Username struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"username"`
|
||||
} `json:"username" cfg:"username"`
|
||||
Password struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"password"`
|
||||
} `json:"password" cfg:"password"`
|
||||
Server struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"server"`
|
||||
} `json:"server" cfg:"server"`
|
||||
PublicServer struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"public_server"`
|
||||
} `json:"public_server" cfg:"public_server"`
|
||||
Client struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"client"`
|
||||
} `json:"client" cfg:"client"`
|
||||
Version struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"version"`
|
||||
} `json:"version" cfg:"version"`
|
||||
Device struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"device"`
|
||||
} `json:"device" cfg:"device"`
|
||||
DeviceId struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"device_id"`
|
||||
} `json:"device_id" cfg:"device_id"`
|
||||
} `json:"jellyfin"`
|
||||
Ui struct{
|
||||
Order []string `json:"order"`
|
||||
Meta Metadata `json:"meta"`
|
||||
Theme struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Options []string `json:"options"`
|
||||
Value string `json:"value" cfg:"theme"`
|
||||
} `json:"theme" cfg:"theme"`
|
||||
Host struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"host"`
|
||||
} `json:"host" cfg:"host"`
|
||||
Port struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value int `json:"value" cfg:"port"`
|
||||
} `json:"port" cfg:"port"`
|
||||
JellyfinLogin struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value bool `json:"value" cfg:"jellyfin_login"`
|
||||
} `json:"jellyfin_login" cfg:"jellyfin_login"`
|
||||
AdminOnly struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value bool `json:"value" cfg:"admin_only"`
|
||||
} `json:"admin_only" cfg:"admin_only"`
|
||||
Username struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"username"`
|
||||
} `json:"username" cfg:"username"`
|
||||
Password struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"password"`
|
||||
} `json:"password" cfg:"password"`
|
||||
Email struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"email"`
|
||||
} `json:"email" cfg:"email"`
|
||||
Debug struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value bool `json:"value" cfg:"debug"`
|
||||
} `json:"debug" cfg:"debug"`
|
||||
ContactMessage struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"contact_message"`
|
||||
} `json:"contact_message" cfg:"contact_message"`
|
||||
HelpMessage struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"help_message"`
|
||||
} `json:"help_message" cfg:"help_message"`
|
||||
SuccessMessage struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"success_message"`
|
||||
} `json:"success_message" cfg:"success_message"`
|
||||
Bs5 struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value bool `json:"value" cfg:"bs5"`
|
||||
} `json:"bs5" cfg:"bs5"`
|
||||
} `json:"ui"`
|
||||
PasswordValidation struct{
|
||||
Order []string `json:"order"`
|
||||
Meta Metadata `json:"meta"`
|
||||
Enabled struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value bool `json:"value" cfg:"enabled"`
|
||||
} `json:"enabled" cfg:"enabled"`
|
||||
MinLength struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"min_length"`
|
||||
} `json:"min_length" cfg:"min_length"`
|
||||
Upper struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"upper"`
|
||||
} `json:"upper" cfg:"upper"`
|
||||
Lower struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"lower"`
|
||||
} `json:"lower" cfg:"lower"`
|
||||
Number struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"number"`
|
||||
} `json:"number" cfg:"number"`
|
||||
Special struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"special"`
|
||||
} `json:"special" cfg:"special"`
|
||||
} `json:"password_validation"`
|
||||
Email struct{
|
||||
Order []string `json:"order"`
|
||||
Meta Metadata `json:"meta"`
|
||||
NoUsername struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value bool `json:"value" cfg:"no_username"`
|
||||
} `json:"no_username" cfg:"no_username"`
|
||||
Use24H struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value bool `json:"value" cfg:"use_24h"`
|
||||
} `json:"use_24h" cfg:"use_24h"`
|
||||
DateFormat struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"date_format"`
|
||||
} `json:"date_format" cfg:"date_format"`
|
||||
Message struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"message"`
|
||||
} `json:"message" cfg:"message"`
|
||||
Method struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Options []string `json:"options"`
|
||||
Value string `json:"value" cfg:"method"`
|
||||
} `json:"method" cfg:"method"`
|
||||
Address struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"address"`
|
||||
} `json:"address" cfg:"address"`
|
||||
From struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"from"`
|
||||
} `json:"from" cfg:"from"`
|
||||
} `json:"email"`
|
||||
PasswordResets struct{
|
||||
Order []string `json:"order"`
|
||||
Meta Metadata `json:"meta"`
|
||||
Enabled struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value bool `json:"value" cfg:"enabled"`
|
||||
} `json:"enabled" cfg:"enabled"`
|
||||
WatchDirectory struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"watch_directory"`
|
||||
} `json:"watch_directory" cfg:"watch_directory"`
|
||||
EmailHtml struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"email_html"`
|
||||
} `json:"email_html" cfg:"email_html"`
|
||||
EmailText struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"email_text"`
|
||||
} `json:"email_text" cfg:"email_text"`
|
||||
Subject struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"subject"`
|
||||
} `json:"subject" cfg:"subject"`
|
||||
} `json:"password_resets"`
|
||||
InviteEmails struct{
|
||||
Order []string `json:"order"`
|
||||
Meta Metadata `json:"meta"`
|
||||
Enabled struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value bool `json:"value" cfg:"enabled"`
|
||||
} `json:"enabled" cfg:"enabled"`
|
||||
EmailHtml struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"email_html"`
|
||||
} `json:"email_html" cfg:"email_html"`
|
||||
EmailText struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"email_text"`
|
||||
} `json:"email_text" cfg:"email_text"`
|
||||
Subject struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"subject"`
|
||||
} `json:"subject" cfg:"subject"`
|
||||
UrlBase struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"url_base"`
|
||||
} `json:"url_base" cfg:"url_base"`
|
||||
} `json:"invite_emails"`
|
||||
Notifications struct{
|
||||
Order []string `json:"order"`
|
||||
Meta Metadata `json:"meta"`
|
||||
Enabled struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value bool `json:"value" cfg:"enabled"`
|
||||
} `json:"enabled" cfg:"enabled"`
|
||||
ExpiryHtml struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"expiry_html"`
|
||||
} `json:"expiry_html" cfg:"expiry_html"`
|
||||
ExpiryText struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"expiry_text"`
|
||||
} `json:"expiry_text" cfg:"expiry_text"`
|
||||
CreatedHtml struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"created_html"`
|
||||
} `json:"created_html" cfg:"created_html"`
|
||||
CreatedText struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"created_text"`
|
||||
} `json:"created_text" cfg:"created_text"`
|
||||
} `json:"notifications"`
|
||||
Mailgun struct{
|
||||
Order []string `json:"order"`
|
||||
Meta Metadata `json:"meta"`
|
||||
ApiUrl struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"api_url"`
|
||||
} `json:"api_url" cfg:"api_url"`
|
||||
ApiKey struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"api_key"`
|
||||
} `json:"api_key" cfg:"api_key"`
|
||||
} `json:"mailgun"`
|
||||
Smtp struct{
|
||||
Order []string `json:"order"`
|
||||
Meta Metadata `json:"meta"`
|
||||
Encryption struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Options []string `json:"options"`
|
||||
Value string `json:"value" cfg:"encryption"`
|
||||
} `json:"encryption" cfg:"encryption"`
|
||||
Server struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"server"`
|
||||
} `json:"server" cfg:"server"`
|
||||
Port struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value int `json:"value" cfg:"port"`
|
||||
} `json:"port" cfg:"port"`
|
||||
Password struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"password"`
|
||||
} `json:"password" cfg:"password"`
|
||||
} `json:"smtp"`
|
||||
Files struct{
|
||||
Order []string `json:"order"`
|
||||
Meta Metadata `json:"meta"`
|
||||
Invites struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"invites"`
|
||||
} `json:"invites" cfg:"invites"`
|
||||
Emails struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"emails"`
|
||||
} `json:"emails" cfg:"emails"`
|
||||
UserTemplate struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"user_template"`
|
||||
} `json:"user_template" cfg:"user_template"`
|
||||
UserConfiguration struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"user_configuration"`
|
||||
} `json:"user_configuration" cfg:"user_configuration"`
|
||||
UserDisplayprefs struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"user_displayprefs"`
|
||||
} `json:"user_displayprefs" cfg:"user_displayprefs"`
|
||||
CustomCss struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"custom_css"`
|
||||
} `json:"custom_css" cfg:"custom_css"`
|
||||
} `json:"files"`
|
||||
}
|
||||
57
config/jsontostruct.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import json
|
||||
|
||||
with open("config-formatted.json", "r") as f:
|
||||
config = json.load(f)
|
||||
|
||||
indent = 0
|
||||
|
||||
|
||||
def writeln(ln):
|
||||
global indent
|
||||
if "}" in ln and "{" not in ln:
|
||||
indent -= 1
|
||||
s.write(("\t" * indent) + ln + "\n")
|
||||
if "{" in ln and "}" not in ln:
|
||||
indent += 1
|
||||
|
||||
|
||||
with open("configStruct.go", "w") as s:
|
||||
writeln("package main")
|
||||
writeln("")
|
||||
writeln("type Metadata struct{")
|
||||
writeln('Name string `json:"name"`')
|
||||
writeln('Description string `json:"description"`')
|
||||
writeln("}")
|
||||
writeln("")
|
||||
writeln("type Config struct{")
|
||||
if "order" in config:
|
||||
writeln('Order []string `json:"order"`')
|
||||
for section in [x for x in config.keys() if x != "order"]:
|
||||
title = "".join([x.title() for x in section.split("_")])
|
||||
writeln(title + " struct{")
|
||||
if "order" in config[section]:
|
||||
writeln('Order []string `json:"order"`')
|
||||
if "meta" in config[section]:
|
||||
writeln('Meta Metadata `json:"meta"`')
|
||||
for setting in [
|
||||
x for x in config[section].keys() if x != "order" and x != "meta"
|
||||
]:
|
||||
name = "".join([x.title() for x in setting.split("_")])
|
||||
writeln(name + " struct{")
|
||||
writeln('Name string `json:"name"`')
|
||||
writeln('Required bool `json:"required"`')
|
||||
writeln('Restart bool `json:"requires_restart"`')
|
||||
writeln('Description string `json:"description"`')
|
||||
writeln('Type string `json:"type"`')
|
||||
dt = config[section][setting]["type"]
|
||||
if dt == "select":
|
||||
dt = "string"
|
||||
writeln('Options []string `json:"options"`')
|
||||
elif dt == "number":
|
||||
dt = "int"
|
||||
elif dt != "bool":
|
||||
dt = "string"
|
||||
writeln(f'Value {dt} `json:"value" cfg:"{setting}"`')
|
||||
writeln("} " + f'`json:"{setting}" cfg:"{setting}"`')
|
||||
writeln("} " + f'`json:"{section}"`')
|
||||
writeln("}")
|
||||
14
daemon.go
@@ -9,21 +9,21 @@ type Repeater struct {
|
||||
ShutdownChannel chan string
|
||||
Interval time.Duration
|
||||
period time.Duration
|
||||
ctx *appContext
|
||||
app *appContext
|
||||
}
|
||||
|
||||
func NewRepeater(interval time.Duration, ctx *appContext) *Repeater {
|
||||
func NewRepeater(interval time.Duration, app *appContext) *Repeater {
|
||||
return &Repeater{
|
||||
Stopped: false,
|
||||
ShutdownChannel: make(chan string),
|
||||
Interval: interval,
|
||||
period: interval,
|
||||
ctx: ctx,
|
||||
app: app,
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *Repeater) Run() {
|
||||
rt.ctx.info.Println("Invite daemon started")
|
||||
rt.app.info.Println("Invite daemon started")
|
||||
for {
|
||||
select {
|
||||
case <-rt.ShutdownChannel:
|
||||
@@ -33,9 +33,9 @@ func (rt *Repeater) Run() {
|
||||
break
|
||||
}
|
||||
started := time.Now()
|
||||
rt.ctx.storage.loadInvites()
|
||||
rt.ctx.debug.Println("Daemon: Checking invites")
|
||||
rt.ctx.checkInvites()
|
||||
rt.app.storage.loadInvites()
|
||||
rt.app.debug.Println("Daemon: Checking invites")
|
||||
rt.app.checkInvites()
|
||||
finished := time.Now()
|
||||
duration := finished.Sub(started)
|
||||
rt.period = rt.Interval - duration
|
||||
|
||||
@@ -184,7 +184,8 @@
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": false
|
||||
"value": false,
|
||||
"description": "Enables debug logging and exposes pprof as a route (Don't use in production!)"
|
||||
},
|
||||
"contact_message": {
|
||||
"name": "Contact message",
|
||||
|
||||
@@ -19,14 +19,25 @@ var transitionEndEvent = whichTransitionEvent();
|
||||
|
||||
// Toggles between light and dark themes
|
||||
function toggleCSS() {
|
||||
let cssEl = document.querySelectorAll('link[rel="stylesheet"][type="text/css"]')[0];
|
||||
let Els = document.querySelectorAll('link[rel="stylesheet"][type="text/css"]');
|
||||
let cssEl = Els[0]
|
||||
let remove = false;
|
||||
if (Els.length != 1) {
|
||||
cssEl = Els[1]
|
||||
remove = true
|
||||
}
|
||||
let href = "bs" + bsVersion;
|
||||
if (cssEl.href.includes(href + "-jf")) {
|
||||
href += ".css";
|
||||
} else {
|
||||
href += "-jf.css";
|
||||
}
|
||||
cssEl.href = href
|
||||
let newEl = cssEl.cloneNode(true);
|
||||
newEl.href = href
|
||||
cssEl.parentNode.insertBefore(newEl, cssEl.nextSibling);
|
||||
if (remove) {
|
||||
Els[0].remove()
|
||||
}
|
||||
document.cookie = "css=" + href;
|
||||
}
|
||||
|
||||
@@ -136,8 +147,8 @@ function addItem(parsedInvite) {
|
||||
|
||||
let code = document.createElement('div');
|
||||
code.classList.add('d-flex', 'align-items-center', 'font-monospace');
|
||||
code.setAttribute('style', 'width: 40%;');
|
||||
let codeLink = document.createElement('a');
|
||||
codeLink.setAttribute('style', 'margin-right: 0.5rem;');
|
||||
codeLink.textContent = parsedInvite[0].replace(/-/g, '-');
|
||||
|
||||
code.appendChild(codeLink);
|
||||
@@ -145,6 +156,7 @@ function addItem(parsedInvite) {
|
||||
listItem.appendChild(code);
|
||||
|
||||
let listRight = document.createElement('div');
|
||||
listRight.setAttribute('style', 'text-align: right;');
|
||||
let listText = document.createElement('span');
|
||||
listText.id = parsedInvite[0] + '_expiry';
|
||||
listText.setAttribute('style', 'margin-right: 1rem;');
|
||||
@@ -156,9 +168,11 @@ function addItem(parsedInvite) {
|
||||
let inviteCode = window.location.href.split('#')[0] + 'invite/' + parsedInvite[0];
|
||||
//
|
||||
codeLink.href = inviteCode;
|
||||
codeLink.classList.add('invite-link');
|
||||
let copyButton = document.createElement('i');
|
||||
copyButton.onclick = function() { toClipboard(inviteCode); };
|
||||
copyButton.classList.add('fa', 'fa-clipboard', 'icon-button');
|
||||
copyButton.setAttribute('style', 'margin-right: 0.5rem; margin-left: 0.5rem;');
|
||||
|
||||
code.appendChild(copyButton);
|
||||
|
||||
@@ -179,9 +193,12 @@ function addItem(parsedInvite) {
|
||||
deleteButton.classList.add('btn', 'btn-outline-danger');
|
||||
deleteButton.textContent = "Delete";
|
||||
|
||||
listRight.appendChild(deleteButton);
|
||||
let block = document.createElement('div');
|
||||
block.setAttribute('style', 'display: inline-block;');
|
||||
block.appendChild(deleteButton);
|
||||
let dropButton = document.createElement('i');
|
||||
dropButton.classList.add('fa', 'fa-angle-down', 'collapsed', 'icon-button', 'not-rotated');
|
||||
dropButton.setAttribute('style', 'padding: 1rem; margin: -1rem -1rem -1rem 0;');
|
||||
dropButton.setAttribute('data-toggle', 'collapse');
|
||||
dropButton.setAttribute('aria-expanded', 'false');
|
||||
dropButton.setAttribute('data-target', '#' + CSS.escape(parsedInvite[0]) + '_collapse');
|
||||
@@ -195,7 +212,8 @@ function addItem(parsedInvite) {
|
||||
}
|
||||
};
|
||||
dropButton.setAttribute('style', 'margin-left: 1rem;');
|
||||
listRight.appendChild(dropButton);
|
||||
block.appendChild(dropButton);
|
||||
listRight.appendChild(block);
|
||||
}
|
||||
|
||||
listItem.appendChild(listRight);
|
||||
@@ -513,28 +531,33 @@ document.getElementById('inviteForm').onsubmit = function() {
|
||||
return false;
|
||||
};
|
||||
|
||||
document.getElementById('loginForm').onsubmit = function() {
|
||||
window.token = "";
|
||||
let details = serializeForm('loginForm');
|
||||
let errorArea = document.getElementById('loginErrorArea');
|
||||
errorArea.textContent = '';
|
||||
let button = document.getElementById('loginSubmit');
|
||||
button.disabled = true;
|
||||
button.innerHTML =
|
||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
||||
'Loading...';
|
||||
function tryLogin(username, password, modal, button, callback) {
|
||||
let req = new XMLHttpRequest();
|
||||
req.responseType = 'json';
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
if (this.status == 401) {
|
||||
button.disabled = false;
|
||||
button.textContent = 'Login';
|
||||
let wrongPassword = document.createElement('div');
|
||||
wrongPassword.classList.add('alert', 'alert-danger');
|
||||
wrongPassword.setAttribute('role', 'alert');
|
||||
wrongPassword.textContent = "Incorrect username or password.";
|
||||
errorArea.appendChild(wrongPassword);
|
||||
if (this.status != 200) {
|
||||
let errormsg = req.response["error"];
|
||||
if (errormsg == "") {
|
||||
errormsg = "Unknown error"
|
||||
}
|
||||
if (modal) {
|
||||
button.disabled = false;
|
||||
button.textContent = errormsg;
|
||||
if (!button.classList.contains('btn-danger')) {
|
||||
button.classList.add('btn-danger');
|
||||
button.classList.remove('btn-primary');
|
||||
}
|
||||
setTimeout(function () {
|
||||
if (button.classList.contains('btn-danger')) {
|
||||
button.classList.add('btn-primary');
|
||||
button.classList.remove('btn-danger');
|
||||
button.textContent = 'Login';
|
||||
}
|
||||
}, 4000)
|
||||
} else {
|
||||
loginModal.show();
|
||||
}
|
||||
} else {
|
||||
const data = this.response;
|
||||
window.token = data['token'];
|
||||
@@ -549,13 +572,37 @@ document.getElementById('loginForm').onsubmit = function() {
|
||||
let minutes = document.getElementById('minutes');
|
||||
addOptions(59, minutes);
|
||||
minutes.selected = "30";
|
||||
loginModal.hide();
|
||||
checkDuration();
|
||||
if (modal) {
|
||||
loginModal.hide();
|
||||
}
|
||||
document.getElementById('logoutButton').setAttribute('style', '');
|
||||
}
|
||||
if (typeof callback === "function") {
|
||||
callback(this.status);
|
||||
}
|
||||
}
|
||||
};
|
||||
req.open("GET", "/getToken", true);
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(details['username'] + ":" + details['password']));
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(username + ":" + password));
|
||||
req.send();
|
||||
}
|
||||
|
||||
document.getElementById('loginForm').onsubmit = function() {
|
||||
window.token = "";
|
||||
let details = serializeForm('loginForm');
|
||||
// let errorArea = document.getElementById('loginErrorArea');
|
||||
// errorArea.textContent = '';
|
||||
let button = document.getElementById('loginSubmit');
|
||||
if (button.classList.contains('btn-danger')) {
|
||||
button.classList.add('btn-primary');
|
||||
button.classList.remove('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...';
|
||||
tryLogin(username = details['username'], password = details['password'], modal = true, button = button)
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -693,10 +740,10 @@ document.getElementById('openUsers').onclick = function () {
|
||||
address.setAttribute('type', 'email');
|
||||
address.readOnly = true;
|
||||
address.classList.add('form-control-plaintext', 'text-muted', 'd-inline-block', 'addressText');
|
||||
address.id = 'address_' + user['email'];
|
||||
address.id = 'address_' + user['name'];
|
||||
address.setAttribute('style', 'width: auto; margin-left: 2%;');
|
||||
if (typeof(user['email']) != 'undefined') {
|
||||
address.value = user['email'];
|
||||
address.setAttribute('style', 'width: auto; margin-left: 2%;');
|
||||
}
|
||||
let editButton = document.createElement('i');
|
||||
editButton.classList.add('fa', 'fa-edit', 'd-inline-block', 'icon-button');
|
||||
@@ -761,7 +808,28 @@ document.getElementById('openUsers').onclick = function () {
|
||||
};
|
||||
|
||||
generateInvites(empty = true);
|
||||
loginModal.show();
|
||||
|
||||
tryLogin("", "", false, callback = function(code){
|
||||
console.log(code);
|
||||
if (code != 200) {
|
||||
loginModal.show();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('logoutButton').onclick = function () {
|
||||
let req = new XMLHttpRequest();
|
||||
req.open("POST", "/logout", true);
|
||||
req.responseType = 'json';
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
window.token = '';
|
||||
location.reload();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
req.send();
|
||||
}
|
||||
|
||||
var config = {};
|
||||
var modifiedConfig = {};
|
||||
@@ -958,3 +1026,17 @@ document.getElementById('settingsSave').onclick = function() {
|
||||
}
|
||||
}
|
||||
|
||||
// Diable 'Generate' button if days, hours, minutes are all zero
|
||||
function checkDuration() {
|
||||
let boxVals = [document.getElementById("days").value, document.getElementById("hours").value, document.getElementById("minutes").value];
|
||||
let submit = document.getElementById("generateSubmit");
|
||||
if (boxVals[0] != 0 || boxVals[1] != 0 || boxVals[2] != 0) {
|
||||
submit.disabled = false;
|
||||
} else if (boxVals[0] == 0 && boxVals[1] == 0 && boxVals[2] == 0) {
|
||||
submit.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (i of ["days", "hours", "minutes"]) {
|
||||
document.getElementById(i).addEventListener("change", checkDuration);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
document.getElementById('page-1').scrollIntoView({
|
||||
behavior: 'auto',
|
||||
block: 'center',
|
||||
inline: 'center' });
|
||||
|
||||
function checkAuthRadio() {
|
||||
if (document.getElementById('manualAuthRadio').checked) {
|
||||
document.getElementById('adminOnlyArea').style.display = 'none';
|
||||
@@ -99,36 +104,55 @@ var jfValid = false
|
||||
document.getElementById('jfTestButton').onclick = function() {
|
||||
var testButton = document.getElementById('jfTestButton');
|
||||
var nextButton = document.getElementById('jfNextButton');
|
||||
testButton.disabled = true;
|
||||
testButton.innerHTML =
|
||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
||||
'Testing...';
|
||||
nextButton.classList.add('disabled');
|
||||
nextButton.setAttribute('aria-disabled', 'true');
|
||||
var jfData = {};
|
||||
jfData['jfHost'] = document.getElementById('jfHost').value;
|
||||
jfData['jfUser'] = document.getElementById('jfUser').value;
|
||||
jfData['jfPassword'] = document.getElementById('jfPassword').value;
|
||||
var req = new XMLHttpRequest();
|
||||
req.open("POST", "/testJF", true);
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.responseType = 'json';
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
testButton.disabled = false;
|
||||
testButton.className = '';
|
||||
if (this.response['success'] == true) {
|
||||
testButton.classList.add('btn', 'btn-success');
|
||||
testButton.textContent = 'Success';
|
||||
nextButton.classList.remove('disabled');
|
||||
nextButton.setAttribute('aria-disabled', 'false');
|
||||
} else {
|
||||
testButton.classList.add('btn', 'btn-danger');
|
||||
testButton.textContent = 'Failed';
|
||||
let valid = true;
|
||||
for (val in jfData) {
|
||||
if (jfData[val] == "") {
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
if (!valid) {
|
||||
if (!testButton.classList.contains('btn-danger')) {
|
||||
testButton.classList.add('btn-danger');
|
||||
testButton.textContent = 'Fill out fields above.';
|
||||
setTimeout(function() {
|
||||
if (testButton.classList.contains('btn-danger')) {
|
||||
testButton.classList.remove('btn-danger');
|
||||
testButton.textContent = 'Test';
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
} else {
|
||||
testButton.disabled = true;
|
||||
testButton.innerHTML =
|
||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
||||
'Testing...';
|
||||
nextButton.classList.add('disabled');
|
||||
nextButton.setAttribute('aria-disabled', 'true');
|
||||
var req = new XMLHttpRequest();
|
||||
req.open("POST", "/testJF", true);
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.responseType = 'json';
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
testButton.disabled = false;
|
||||
testButton.className = '';
|
||||
if (this.response['success'] == true) {
|
||||
testButton.classList.add('btn', 'btn-success');
|
||||
testButton.textContent = 'Success';
|
||||
nextButton.classList.remove('disabled');
|
||||
nextButton.setAttribute('aria-disabled', 'false');
|
||||
} else {
|
||||
testButton.classList.add('btn', 'btn-danger');
|
||||
testButton.textContent = 'Failed';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
req.send(JSON.stringify(jfData));
|
||||
};
|
||||
req.send(JSON.stringify(jfData));
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('submitButton').onclick = function() {
|
||||
@@ -162,6 +186,10 @@ document.getElementById('submitButton').onclick = function() {
|
||||
};
|
||||
// Page 3: Connect to jellyfin
|
||||
config['jellyfin']['server'] = document.getElementById('jfHost').value;
|
||||
let publicAddress = document.getElementById('jfPublicHost').value;
|
||||
if (publicAddress != "") {
|
||||
config['jellyfin']['public_server'] = publicAddress;
|
||||
}
|
||||
config['jellyfin']['username'] = document.getElementById('jfUser').value;
|
||||
config['jellyfin']['password'] = document.getElementById('jfPassword').value;
|
||||
// Page 4: Email (Page 5, 6, 7 are only used if this is enabled)
|
||||
|
||||
@@ -35,16 +35,22 @@
|
||||
{{ else }}
|
||||
const bsVersion = 4;
|
||||
{{ end }}
|
||||
const cssFile = "{{ .cssFile }}";
|
||||
var cssFile = "{{ .cssFile }}";
|
||||
var css = document.createElement('link');
|
||||
css.setAttribute('rel', 'stylesheet');
|
||||
css.setAttribute('type', 'text/css');
|
||||
var cssCookie = getCookie("css");
|
||||
if (cssCookie.includes('bs' + bsVersion)) {
|
||||
css.setAttribute('href', cssCookie);
|
||||
} else {
|
||||
css.setAttribute('href', cssFile);
|
||||
};
|
||||
cssFile = cssCookie;
|
||||
} else if (cssCookie.includes('bs')) {
|
||||
if (cssCookie.includes('jf')) {
|
||||
cssFile = 'bs' + bsVersion + '-jf.css';
|
||||
} else {
|
||||
cssFile = 'bs' + bsVersion + '.css';
|
||||
}
|
||||
document.cookie = 'css=' + cssFile;
|
||||
}
|
||||
css.setAttribute('href', cssFile);
|
||||
document.head.appendChild(css);
|
||||
</script>
|
||||
{{ if not .bs5 }}
|
||||
@@ -117,6 +123,13 @@
|
||||
-o-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||
transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000); /* easeInOutQuart */
|
||||
}
|
||||
|
||||
.invite-link {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
}
|
||||
</style>
|
||||
<title>Admin</title>
|
||||
</head>
|
||||
@@ -136,7 +149,6 @@
|
||||
<input type="password" class="form-control" id="password" name="password" placeholder="Password" required>
|
||||
</div>
|
||||
</form>
|
||||
<div id="loginErrorArea"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" id="loginSubmit" class="btn btn-primary" form="loginForm">Login</button>
|
||||
@@ -252,6 +264,9 @@
|
||||
<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;">
|
||||
Logout <i class="fa fa-sign-out"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card mb-3 linkGroup">
|
||||
<div class="card-header">Current Invites</div>
|
||||
@@ -264,7 +279,7 @@
|
||||
<div class="card-body">
|
||||
<form action="#" method="POST" id="inviteForm" class="container">
|
||||
<div class="row align-items-start">
|
||||
<div class="col">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label for="days">Days</label>
|
||||
<select class="form-control form-select" id="days" name="days">
|
||||
|
||||
@@ -3,12 +3,10 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-serialize-object/2.5.0/jquery.serialize-object.min.js" integrity="sha256-E8KRdFk/LTaaCBoQIV/rFNc0s3ICQQiOHFT4Cioifa8=" crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="bs5-jf.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/js/bootstrap.min.js" integrity="sha384-oesi62hOLfzrys4LxRF63OJCXdXDipiYWBnvTl9Y9/TRlw5xlKIEHpNyvvDShgf/" crossorigin="anonymous"></script>
|
||||
<style>
|
||||
.card-body {
|
||||
width: 100%;
|
||||
@@ -45,7 +43,7 @@
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Welcome!</h5>
|
||||
<p class="card-text">
|
||||
You'll need to do a few things to start using jellyfin-accounts. Click below to get started, or quit and edit the config file manually.
|
||||
You'll need to do a few things to start using jfa-go. Click below to get started, or quit and edit the config file manually.
|
||||
</p>
|
||||
<a class="btn btn-primary nextButton" href="#page-2">Get Started</a>
|
||||
</div>
|
||||
@@ -59,7 +57,7 @@
|
||||
<p class="card-text">
|
||||
To access the admin page, you'll need to login. Choose how below.
|
||||
<ul>
|
||||
<li><b>Authorize through Jellyfin: </b>Checks credentials with jellyfin, allowing you to share login details and grant multiple users access.</li>
|
||||
<li><b>Authorize through Jellyfin: </b>Checks credentials with Jellyfin, allowing you to share login details and grant multiple users access.</li>
|
||||
<li><b>Username & Password: </b>Set your own username and password manually.</li>
|
||||
</ul>
|
||||
<div class="form-check" id="jfAuthFormGroup">
|
||||
@@ -106,31 +104,37 @@
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Jellyfin</h5>
|
||||
<p class="card-text">
|
||||
jellyfin-accounts needs admin access so that it can create users.
|
||||
jfa-go needs admin access so that it can create users, as this is currently not permitted via API tokens.
|
||||
You should create a separate account for it, checking 'Allow this user to manage the server'. You can disable everything else. Once done, enter the credentials here.
|
||||
<div class="form-group">
|
||||
<label for="jfHost">Host</label>
|
||||
<input type="url" class="form-control" id="jfHost" placeholder="http://jellyf.in:443">
|
||||
<label for="jfHost">Host (For internal use)</label>
|
||||
<input type="url" class="form-control" id="jfHost" placeholder="http://jellyf.in:443" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="jfPublicHost">Public Host (For access by users)</label>
|
||||
<input type="url" class="form-control" id="jfPublicHost" placeholder="Leave blank to use the above address.">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="jfUser">Username</label>
|
||||
<input type="text" class="form-control" id="jfUser" placeholder="Username">
|
||||
<input type="text" class="form-control" id="jfUser" placeholder="Username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="jfPassword">Password</label>
|
||||
<input type="password" class="form-control" id="jfPassword" placeholder="Password">
|
||||
<input type="password" class="form-control" id="jfPassword" placeholder="Password" required>
|
||||
</div>
|
||||
<button class="btn btn-secondary" id="jfTestButton">Test</button>
|
||||
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
|
||||
<a class="btn btn-secondary backButton" href="#page-2">Back</a>
|
||||
<a class="btn btn-primary nextButton disabled" id="jfNextButton" aria-disabled="true" href="#page-4">Next</a>
|
||||
<div style="margin-top: 1rem;">
|
||||
<button class="btn btn-secondary" id="jfTestButton">Test</button>
|
||||
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
|
||||
<a class="btn btn-secondary backButton" href="#page-2">Back</a>
|
||||
<a class="btn btn-primary nextButton disabled" id="jfNextButton" aria-disabled="true" href="#page-4">Next</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slide card" id="page-4">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Email</h5>
|
||||
<p class="card-text">jellyfin-accounts is capable of sending a PIN code when a user tries to reset their password on Jellyfin. One can also choose to send an invite code directly to an email address. This can be done through SMTP or through <a href="https://www.mailgun.com/">Mailgun's</a> API.
|
||||
<p class="card-text">jfa-go is capable of sending a PIN code when a user tries to reset their password on Jellyfin. One can also choose to send an invite code directly to an email address. This can be done through SMTP or through <a href="https://www.mailgun.com/">Mailgun's</a> API.
|
||||
<div class="form-group">
|
||||
<div class="form-check" id="emailDisabled">
|
||||
<input class="form-check-input" type="radio" name="email" id="emailDisabledRadio" value="emailDisabled">
|
||||
@@ -152,9 +156,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="emailSMTPArea">
|
||||
<div class="form-group form-check">
|
||||
<input type="checkbox" class="custom-control-input" id="emailSSL_TLS" checked>
|
||||
<label for="emailSSL_TLS" class="custom-control-label" id="emailSSL_TLSLabel">Use SSL/TLS</label>
|
||||
<div class="form-group form-check form-switch">
|
||||
<input type="checkbox" class="form-check-input" id="emailSSL_TLS" checked>
|
||||
<label for="emailSSL_TLS" class="form-check-label" id="emailSSL_TLSLabel">Use SSL/TLS</label>
|
||||
<small class="form-text text-muted">Note: SSL/TLS usually uses port 465, whereas STARTTLS usually uses 587.</small>
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
@@ -236,7 +240,7 @@
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Password Resets</h5>
|
||||
<p class="card-text">
|
||||
When a user tries to reset their password in jellyfin, it informs them that a file has been created, named "passwordreset*.json" where * is a number. jellyfin-accounts will then read this file, and send the PIN to the user's email. Try it now, and put the folder that it informs you it put the file in below. Also, if enter a custom email subject if you don't like the default one.
|
||||
When a user tries to reset their password in jellyfin, it informs them that a file has been created, named "passwordreset*.json" where * is a number. jfa-go will then read this file, and send the PIN to the user's email. Try it now, and put the folder that it informs you it put the file in below. Also, if enter a custom email subject if you don't like the default one.
|
||||
</p>
|
||||
<div class="form-group form-check">
|
||||
<input type="checkbox" class="form-check-input" id="pwrEnabled" value="enabled">
|
||||
|
||||
91
email.go
@@ -2,15 +2,17 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
// "context"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"github.com/knz/strtime"
|
||||
"github.com/mailgun/mailgun-go/v4"
|
||||
"html/template"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
jEmail "github.com/jordan-wright/email"
|
||||
"github.com/knz/strtime"
|
||||
"github.com/mailgun/mailgun-go/v4"
|
||||
)
|
||||
|
||||
type Emailer struct {
|
||||
@@ -18,6 +20,8 @@ type Emailer struct {
|
||||
sendType, sendMethod, fromAddr, fromName string
|
||||
content Email
|
||||
mg *mailgun.MailgunImpl
|
||||
mime string
|
||||
host string
|
||||
}
|
||||
|
||||
type Email struct {
|
||||
@@ -46,31 +50,35 @@ func (email *Emailer) formatExpiry(expiry time.Time, tzaware bool, datePattern,
|
||||
return
|
||||
}
|
||||
|
||||
func (email *Emailer) init(ctx *appContext) {
|
||||
email.fromAddr = ctx.config.Section("email").Key("address").String()
|
||||
email.fromName = ctx.config.Section("email").Key("from").String()
|
||||
email.sendMethod = ctx.config.Section("email").Key("method").String()
|
||||
func (email *Emailer) init(app *appContext) {
|
||||
email.fromAddr = app.config.Section("email").Key("address").String()
|
||||
email.fromName = app.config.Section("email").Key("from").String()
|
||||
email.sendMethod = app.config.Section("email").Key("method").String()
|
||||
if email.sendMethod == "mailgun" {
|
||||
email.mg = mailgun.NewMailgun(strings.Split(email.fromAddr, "@")[1], ctx.config.Section("mailgun").Key("api_key").String())
|
||||
api_url := ctx.config.Section("mailgun").Key("api_url").String()
|
||||
email.mg = mailgun.NewMailgun(strings.Split(email.fromAddr, "@")[1], app.config.Section("mailgun").Key("api_key").String())
|
||||
api_url := app.config.Section("mailgun").Key("api_url").String()
|
||||
// Mailgun client takes the base url, so we need to trim off the end (e.g 'v3/messages'
|
||||
if strings.Contains(api_url, "messages") {
|
||||
api_url = api_url[0:strings.LastIndex(api_url, "/")]
|
||||
api_url = api_url[0:strings.LastIndex(api_url, "/")]
|
||||
}
|
||||
email.mg.SetAPIBase(api_url)
|
||||
} else if email.sendMethod == "smtp" {
|
||||
app.host = app.config.Section("smtp").Key("server").String()
|
||||
email.smtpAuth = smtp.PlainAuth("", email.fromAddr, app.config.Section("smtp").Key("password").String(), app.host)
|
||||
}
|
||||
}
|
||||
|
||||
func (email *Emailer) constructInvite(code string, invite Invite, ctx *appContext) error {
|
||||
email.content.subject = ctx.config.Section("invite_emails").Key("subject").String()
|
||||
func (email *Emailer) constructInvite(code string, invite Invite, app *appContext) error {
|
||||
email.content.subject = app.config.Section("invite_emails").Key("subject").String()
|
||||
expiry := invite.ValidTill
|
||||
d, t, expires_in := email.formatExpiry(expiry, false, ctx.datePattern, ctx.timePattern)
|
||||
message := ctx.config.Section("email").Key("message").String()
|
||||
invite_link := ctx.config.Section("invite_emails").Key("url_base").String()
|
||||
d, t, expires_in := email.formatExpiry(expiry, false, app.datePattern, app.timePattern)
|
||||
message := app.config.Section("email").Key("message").String()
|
||||
invite_link := app.config.Section("invite_emails").Key("url_base").String()
|
||||
invite_link = fmt.Sprintf("%s/%s", invite_link, code)
|
||||
|
||||
for _, key := range []string{"html", "text"} {
|
||||
fpath := ctx.config.Section("invite_emails").Key("email_" + key).String()
|
||||
fpath := app.config.Section("invite_emails").Key("email_" + key).String()
|
||||
tpl, err := template.ParseFiles(fpath)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -96,11 +104,11 @@ func (email *Emailer) constructInvite(code string, invite Invite, ctx *appContex
|
||||
return nil
|
||||
}
|
||||
|
||||
func (email *Emailer) constructExpiry(code string, invite Invite, ctx *appContext) error {
|
||||
func (email *Emailer) constructExpiry(code string, invite Invite, app *appContext) error {
|
||||
email.content.subject = "Notice: Invite expired"
|
||||
expiry := ctx.formatDatetime(invite.ValidTill)
|
||||
expiry := app.formatDatetime(invite.ValidTill)
|
||||
for _, key := range []string{"html", "text"} {
|
||||
fpath := ctx.config.Section("notifications").Key("expiry_" + key).String()
|
||||
fpath := app.config.Section("notifications").Key("expiry_" + key).String()
|
||||
tpl, err := template.ParseFiles(fpath)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -123,17 +131,17 @@ func (email *Emailer) constructExpiry(code string, invite Invite, ctx *appContex
|
||||
return nil
|
||||
}
|
||||
|
||||
func (email *Emailer) constructCreated(code, username, address string, invite Invite, ctx *appContext) error {
|
||||
func (email *Emailer) constructCreated(code, username, address string, invite Invite, app *appContext) error {
|
||||
email.content.subject = "Notice: User created"
|
||||
created := ctx.formatDatetime(invite.Created)
|
||||
created := app.formatDatetime(invite.Created)
|
||||
var tplAddress string
|
||||
if ctx.config.Section("email").Key("no_username").MustBool(false) {
|
||||
if app.config.Section("email").Key("no_username").MustBool(false) {
|
||||
tplAddress = "n/a"
|
||||
} else {
|
||||
tplAddress = address
|
||||
}
|
||||
for _, key := range []string{"html", "text"} {
|
||||
fpath := ctx.config.Section("notifications").Key("created_" + key).String()
|
||||
fpath := app.config.Section("notifications").Key("created_" + key).String()
|
||||
tpl, err := template.ParseFiles(fpath)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -158,12 +166,12 @@ func (email *Emailer) constructCreated(code, username, address string, invite In
|
||||
return nil
|
||||
}
|
||||
|
||||
func (email *Emailer) constructReset(pwr Pwr, ctx *appContext) error {
|
||||
email.content.subject = ctx.config.Section("password_resets").Key("subject").MustString("Password reset - Jellyfin")
|
||||
d, t, expires_in := email.formatExpiry(pwr.Expiry, true, ctx.datePattern, ctx.timePattern)
|
||||
message := ctx.config.Section("email").Key("message").String()
|
||||
func (email *Emailer) constructReset(pwr Pwr, app *appContext) error {
|
||||
email.content.subject = app.config.Section("password_resets").Key("subject").MustString("Password reset - Jellyfin")
|
||||
d, t, expires_in := email.formatExpiry(pwr.Expiry, true, app.datePattern, app.timePattern)
|
||||
message := app.config.Section("email").Key("message").String()
|
||||
for _, key := range []string{"html", "text"} {
|
||||
fpath := ctx.config.Section("password_resets").Key("email_" + key).String()
|
||||
fpath := app.config.Section("password_resets").Key("email_" + key).String()
|
||||
tpl, err := template.ParseFiles(fpath)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -190,7 +198,7 @@ func (email *Emailer) constructReset(pwr Pwr, ctx *appContext) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (email *Emailer) send(address string, ctx *appContext) error {
|
||||
func (email *Emailer) send(address string, app *appContext) error {
|
||||
if email.sendMethod == "mailgun" {
|
||||
message := email.mg.NewMessage(
|
||||
fmt.Sprintf("%s <%s>", email.fromName, email.fromAddr),
|
||||
@@ -198,12 +206,35 @@ func (email *Emailer) send(address string, ctx *appContext) error {
|
||||
email.content.text,
|
||||
address)
|
||||
message.SetHtml(email.content.html)
|
||||
mgctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||
mgapp, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||
defer cancel()
|
||||
_, _, err := email.mg.Send(mgctx, message)
|
||||
_, _, err := email.mg.Send(mgapp, message)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if email.sendMethod == "smtp" {
|
||||
e := jEmail.NewEmail()
|
||||
e.Subject = email.content.subject
|
||||
e.From = fmt.Sprintf("%s <%s>", email.fromName, email.fromAddr)
|
||||
e.To = []string{address}
|
||||
e.Text = []byte(email.content.text)
|
||||
e.HTML = []byte(email.content.html)
|
||||
smtpType := app.config.Section("smtp").Key("encryption").String()
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: false,
|
||||
ServerName: app.host,
|
||||
}
|
||||
var err error
|
||||
if smtpType == "ssl_tls" {
|
||||
port := app.config.Section("smtp").Key("port").MustInt(465)
|
||||
server := fmt.Sprintf("%s:%d", app.host, port)
|
||||
err = e.SendWithTLS(server, email.smtpAuth, tlsConfig)
|
||||
} else if smtpType == "starttls" {
|
||||
port := app.config.Section("smtp").Key("port").MustInt(587)
|
||||
server := fmt.Sprintf("%s:%d", app.host, port)
|
||||
e.SendWithStartTLS(server, email.smtpAuth, tlsConfig)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
10
go.mod
@@ -5,21 +5,23 @@ go 1.14
|
||||
require (
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/fsnotify/fsnotify v1.4.9
|
||||
github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2
|
||||
github.com/gin-contrib/pprof v1.3.0
|
||||
github.com/gin-contrib/static v0.0.0-20200815103939-31fb0c56a3d1
|
||||
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/json-iterator/go v1.1.10 // indirect
|
||||
github.com/knz/strtime v0.0.0-20200318182718-be999391ffa9
|
||||
github.com/lithammer/shortuuid/v3 v3.0.4
|
||||
github.com/mailgun/mailgun-go/v4 v4.1.3
|
||||
github.com/mailru/easyjson v0.7.2 // indirect
|
||||
github.com/mailru/easyjson v0.7.3 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1 // indirect
|
||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed // indirect
|
||||
google.golang.org/protobuf v1.25.0 // indirect
|
||||
gopkg.in/ini.v1 v1.57.0
|
||||
gopkg.in/ini.v1 v1.60.0
|
||||
gopkg.in/yaml.v2 v2.3.0 // indirect
|
||||
)
|
||||
|
||||
13
go.sum
@@ -15,11 +15,16 @@ 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/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.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-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=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/go-chi/chi v4.0.0+incompatible h1:SiLLEDyAkqNnw+T/uDTf3aFB9T4FTrwMpuYrgaRcnW4=
|
||||
@@ -59,6 +64,8 @@ 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/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/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.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
@@ -83,6 +90,8 @@ 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/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=
|
||||
@@ -128,6 +137,8 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtD
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -160,6 +171,8 @@ gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8
|
||||
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/yaml.v2 v2.2.2/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=
|
||||
|
||||
6
images/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Images
|
||||
|
||||
This holds any images on the main README, and the base files for the icons and banner. The font used, like Jellyfin, is [Quicksand](https://fonts.google.com/specimen/Quicksand) by Andrew Paglinawan.
|
||||
|
||||
"Go" text logo and Gopher image: Copyright 2018 The Go Authors. All rights reserved.
|
||||
https://creativecommons.org/licenses/by/3.0/legalcode
|
||||
BIN
images/admin.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
images/create.png
Normal file
|
After Width: | Height: | Size: 106 KiB |
283
images/jfa-go-banner-wide.svg
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
images/jfa-go-icon.png
Executable file
|
After Width: | Height: | Size: 91 KiB |
22
images/jfa-go-icon.svg
Executable file
|
After Width: | Height: | Size: 113 KiB |
BIN
images/jfa-go-social.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
337
images/jfa-go-social.svg
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
images/jfa.gif
Normal file
|
After Width: | Height: | Size: 3.2 MiB |
25
invites.json
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"Ucfde4vQQxFFVbN8zopiCA": {
|
||||
"created": "28/7/20 14:40",
|
||||
"no-limit": true,
|
||||
"valid_till": "2020-07-29T14:40:40.100081",
|
||||
"email": "hrfee@pm.me",
|
||||
"used-by": [
|
||||
[
|
||||
"test@test.com",
|
||||
"28/7/20 14:40"
|
||||
]
|
||||
],
|
||||
"notify": {
|
||||
"harveyltindall@gmail.com": {
|
||||
"notify-expiry": true,
|
||||
"notify-creation": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"kr6M9PirRk0gmlCgaJTB-g": {
|
||||
"created": "28/7/20 14:41",
|
||||
"remaining-uses": 20,
|
||||
"valid_till": "2020-07-29T14:41:18.130088"
|
||||
}
|
||||
}
|
||||
53
jfapi.go
@@ -37,15 +37,25 @@ type Jellyfin struct {
|
||||
userId string
|
||||
httpClient *http.Client
|
||||
loginParams map[string]string
|
||||
userCache []map[string]interface{}
|
||||
cacheExpiry time.Time
|
||||
cacheLength int
|
||||
noFail bool
|
||||
}
|
||||
|
||||
func (jf *Jellyfin) timeoutHandler() {
|
||||
if r := recover(); r != nil {
|
||||
log.Fatalf("Failed to authenticate with Jellyfin @ %s: Timed out", jf.server)
|
||||
out := fmt.Sprintf("Failed to authenticate with Jellyfin @ %s: Timed out", jf.server)
|
||||
if jf.noFail {
|
||||
log.Printf(out)
|
||||
} else {
|
||||
log.Fatalf(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (jf *Jellyfin) init(server, client, version, device, deviceId string) error {
|
||||
func newJellyfin(server, client, version, device, deviceId string) (*Jellyfin, error) {
|
||||
jf := &Jellyfin{}
|
||||
jf.server = server
|
||||
jf.client = client
|
||||
jf.version = version
|
||||
@@ -73,7 +83,9 @@ func (jf *Jellyfin) init(server, client, version, device, deviceId string) error
|
||||
data, _ := ioutil.ReadAll(resp.Body)
|
||||
json.Unmarshal(data, &jf.serverInfo)
|
||||
}
|
||||
return nil
|
||||
jf.cacheLength = 30
|
||||
jf.cacheExpiry = time.Now()
|
||||
return jf, nil
|
||||
}
|
||||
|
||||
func (jf *Jellyfin) authenticate(username, password string) (map[string]interface{}, int, error) {
|
||||
@@ -199,20 +211,24 @@ func (jf *Jellyfin) getUsers(public bool) ([]map[string]interface{}, int, error)
|
||||
var data io.Reader
|
||||
var status int
|
||||
var err error
|
||||
if public {
|
||||
url := fmt.Sprintf("%s/emby/Users/Public", jf.server)
|
||||
data, status, err = jf._getReader(url, nil)
|
||||
if time.Now().After(jf.cacheExpiry) {
|
||||
if public {
|
||||
url := fmt.Sprintf("%s/emby/Users/Public", jf.server)
|
||||
data, status, err = jf._getReader(url, nil)
|
||||
|
||||
} else {
|
||||
url := fmt.Sprintf("%s/emby/Users", jf.server)
|
||||
data, status, err = jf._getReader(url, jf.loginParams)
|
||||
} else {
|
||||
url := fmt.Sprintf("%s/emby/Users", jf.server)
|
||||
data, status, err = jf._getReader(url, jf.loginParams)
|
||||
}
|
||||
if err != nil || status != 200 {
|
||||
return nil, status, err
|
||||
}
|
||||
json.NewDecoder(data).Decode(&result)
|
||||
jf.userCache = result
|
||||
jf.cacheExpiry = time.Now().Add(time.Minute * time.Duration(jf.cacheLength))
|
||||
return result, status, nil
|
||||
}
|
||||
if err != nil || status != 200 {
|
||||
return nil, status, err
|
||||
}
|
||||
json.NewDecoder(data).Decode(&result)
|
||||
return result, status, nil
|
||||
|
||||
return jf.userCache, 200, nil
|
||||
}
|
||||
|
||||
func (jf *Jellyfin) userByName(username string, public bool) (map[string]interface{}, int, error) {
|
||||
@@ -229,6 +245,13 @@ func (jf *Jellyfin) userByName(username string, public bool) (map[string]interfa
|
||||
}
|
||||
|
||||
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 {
|
||||
return user, 200, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if public {
|
||||
users, status, err := jf.getUsers(public)
|
||||
if err != nil || status != 200 {
|
||||
|
||||
@@ -1,34 +1,60 @@
|
||||
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":
|
||||
return subprocess.check_output(cmd, shell=True)
|
||||
proc = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE)
|
||||
return proc.communicate()
|
||||
|
||||
local_path = Path(__file__).resolve().parent
|
||||
node_bin = local_path.parent / 'node_modules' / '.bin'
|
||||
|
||||
for mjml in [f for f in local_path.iterdir() if f.is_file() and 'mjml' in f.suffix]:
|
||||
print(f'Compiling {mjml.name}')
|
||||
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)}')
|
||||
if fname.is_file():
|
||||
print('Done.')
|
||||
print("Done.")
|
||||
|
||||
html = [f for f in local_path.iterdir() if f.is_file() and 'html' in f.suffix]
|
||||
html = [f for f in local_path.iterdir() if f.is_file() and "html" in f.suffix]
|
||||
|
||||
output = local_path.parent / 'data'
|
||||
output = local_path.parent / "data"
|
||||
|
||||
for f in html:
|
||||
shutil.copy(str(f),
|
||||
str(output / f.name))
|
||||
print(f'Copied {f.name} to {str(output / f.name)}')
|
||||
txtfile = f.with_suffix('.txt')
|
||||
shutil.copy(str(f), str(output / f.name))
|
||||
print(f"Copied {f.name} to {str(output / f.name)}")
|
||||
txtfile = f.with_suffix(".txt")
|
||||
if txtfile.is_file():
|
||||
shutil.copy(str(txtfile),
|
||||
str(output / txtfile.name))
|
||||
print(f'Copied {txtfile.name} to {str(output / txtfile.name)}')
|
||||
shutil.copy(str(txtfile), str(output / txtfile.name))
|
||||
print(f"Copied {txtfile.name} to {str(output / txtfile.name)}")
|
||||
else:
|
||||
print(f'Warning: {txtfile.name} does not exist. Text versions of emails should be supplied.')
|
||||
|
||||
print(
|
||||
f"Warning: {txtfile.name} does not exist. Text versions of emails should be supplied."
|
||||
)
|
||||
|
||||
310
main.go
@@ -1,21 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/pprof"
|
||||
"github.com/gin-contrib/static"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
"gopkg.in/ini.v1"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Username is JWT!
|
||||
@@ -26,6 +31,7 @@ type User struct {
|
||||
}
|
||||
|
||||
type appContext struct {
|
||||
// defaults *Config
|
||||
config *ini.File
|
||||
config_path string
|
||||
configBase_path string
|
||||
@@ -36,8 +42,9 @@ type appContext struct {
|
||||
bsVersion int
|
||||
jellyfinLogin bool
|
||||
users []User
|
||||
jf Jellyfin
|
||||
authJf Jellyfin
|
||||
invalidTokens []string
|
||||
jf *Jellyfin
|
||||
authJf *Jellyfin
|
||||
datePattern string
|
||||
timePattern string
|
||||
storage Storage
|
||||
@@ -47,6 +54,7 @@ type appContext struct {
|
||||
host string
|
||||
port int
|
||||
version string
|
||||
quit chan os.Signal
|
||||
}
|
||||
|
||||
func GenerateSecret(length int) (string, error) {
|
||||
@@ -75,7 +83,6 @@ func setGinLogger(router *gin.Engine, debugMode bool) {
|
||||
}(),
|
||||
)
|
||||
}))
|
||||
gin.SetMode(gin.DebugMode)
|
||||
} else {
|
||||
router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
|
||||
return fmt.Sprintf("[GIN] %s(%s) => %d\n",
|
||||
@@ -84,234 +91,281 @@ func setGinLogger(router *gin.Engine, debugMode bool) {
|
||||
param.StatusCode,
|
||||
)
|
||||
}))
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
ctx := new(appContext)
|
||||
// app encompasses essentially all useful functions.
|
||||
app := new(appContext)
|
||||
|
||||
/*
|
||||
set default config, data and local paths
|
||||
also, confusing naming here. data_path is not the internal 'data' directory, rather the users .config/jfa-go folder.
|
||||
local_path is the internal 'data' directory.
|
||||
*/
|
||||
userConfigDir, _ := os.UserConfigDir()
|
||||
ctx.data_path = filepath.Join(userConfigDir, "jfa-go")
|
||||
ctx.config_path = filepath.Join(ctx.data_path, "config.ini")
|
||||
ctx.local_path = "data"
|
||||
app.data_path = filepath.Join(userConfigDir, "jfa-go")
|
||||
app.config_path = filepath.Join(app.data_path, "config.ini")
|
||||
executable, _ := os.Executable()
|
||||
app.local_path = filepath.Join(filepath.Dir(executable), "data")
|
||||
|
||||
ctx.info = log.New(os.Stdout, "[INFO] ", log.Ltime)
|
||||
ctx.err = log.New(os.Stdout, "[ERROR] ", log.Ltime|log.Lshortfile)
|
||||
app.info = log.New(os.Stdout, "[INFO] ", log.Ltime)
|
||||
app.err = log.New(os.Stdout, "[ERROR] ", log.Ltime|log.Lshortfile)
|
||||
|
||||
dataPath := flag.String("data", ctx.data_path, "alternate path to data directory.")
|
||||
configPath := flag.String("config", ctx.config_path, "alternate path to config file.")
|
||||
dataPath := flag.String("data", app.data_path, "alternate path to data directory.")
|
||||
configPath := flag.String("config", app.config_path, "alternate path to config file.")
|
||||
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.")
|
||||
|
||||
flag.Parse()
|
||||
if ctx.config_path == *configPath && ctx.data_path != *dataPath {
|
||||
ctx.config_path = filepath.Join(*dataPath, "config.ini")
|
||||
|
||||
// attempt to apply command line flags correctly
|
||||
if app.config_path == *configPath && app.data_path != *dataPath {
|
||||
app.data_path = *dataPath
|
||||
app.config_path = filepath.Join(app.data_path, "config.ini")
|
||||
} else if app.config_path != *configPath && app.data_path == *dataPath {
|
||||
app.config_path = *configPath
|
||||
} else {
|
||||
ctx.config_path = *configPath
|
||||
ctx.data_path = *dataPath
|
||||
app.config_path = *configPath
|
||||
app.data_path = *dataPath
|
||||
}
|
||||
|
||||
// Env variables are necessary because syscall.Exec for self-restarts doesn't doesn't work with arguments for some reason.
|
||||
// env variables are necessary because syscall.Exec for self-restarts doesn't doesn't work with arguments for some reason.
|
||||
|
||||
if v := os.Getenv("JFA_CONFIGPATH"); v != "" {
|
||||
ctx.config_path = v
|
||||
app.config_path = v
|
||||
}
|
||||
if v := os.Getenv("JFA_DATAPATH"); v != "" {
|
||||
ctx.data_path = v
|
||||
app.data_path = v
|
||||
}
|
||||
|
||||
os.Setenv("JFA_CONFIGPATH", ctx.config_path)
|
||||
os.Setenv("JFA_DATAPATH", ctx.data_path)
|
||||
os.Setenv("JFA_CONFIGPATH", app.config_path)
|
||||
os.Setenv("JFA_DATAPATH", app.data_path)
|
||||
|
||||
var firstRun bool
|
||||
if _, err := os.Stat(ctx.data_path); os.IsNotExist(err) {
|
||||
os.Mkdir(ctx.data_path, 0700)
|
||||
if _, err := os.Stat(app.data_path); os.IsNotExist(err) {
|
||||
os.Mkdir(app.data_path, 0700)
|
||||
}
|
||||
if _, err := os.Stat(ctx.config_path); os.IsNotExist(err) {
|
||||
if _, err := os.Stat(app.config_path); os.IsNotExist(err) {
|
||||
firstRun = true
|
||||
dConfigPath := filepath.Join(ctx.local_path, "config-default.ini")
|
||||
dConfigPath := filepath.Join(app.local_path, "config-default.ini")
|
||||
var dConfig *os.File
|
||||
dConfig, err = os.Open(dConfigPath)
|
||||
if err != nil {
|
||||
ctx.err.Fatalf("Couldn't find default config file \"%s\"", dConfigPath)
|
||||
app.err.Fatalf("Couldn't find default config file \"%s\"", dConfigPath)
|
||||
}
|
||||
defer dConfig.Close()
|
||||
var nConfig *os.File
|
||||
nConfig, err := os.Create(ctx.config_path)
|
||||
nConfig, err := os.Create(app.config_path)
|
||||
if err != nil {
|
||||
ctx.err.Fatalf("Couldn't open config file for writing: \"%s\"", dConfigPath)
|
||||
app.err.Fatalf("Couldn't open config file for writing: \"%s\"", app.config_path)
|
||||
}
|
||||
defer nConfig.Close()
|
||||
_, err = io.Copy(nConfig, dConfig)
|
||||
if err != nil {
|
||||
ctx.err.Fatalf("Couldn't copy default config. To do this manually, copy\n%s\nto\n%s", dConfigPath, ctx.config_path)
|
||||
app.err.Fatalf("Couldn't copy default config. To do this manually, copy\n%s\nto\n%s", dConfigPath, app.config_path)
|
||||
}
|
||||
ctx.info.Printf("Copied default configuration to \"%s\"", ctx.config_path)
|
||||
app.info.Printf("Copied default configuration to \"%s\"", app.config_path)
|
||||
}
|
||||
|
||||
var debugMode bool
|
||||
var address string
|
||||
if ctx.loadConfig() != nil {
|
||||
ctx.err.Fatalf("Failed to load config file \"%s\"", ctx.config_path)
|
||||
if app.loadConfig() != nil {
|
||||
app.err.Fatalf("Failed to load config file \"%s\"", app.config_path)
|
||||
}
|
||||
app.version = app.config.Section("jellyfin").Key("version").String()
|
||||
// read from config...
|
||||
debugMode = app.config.Section("ui").Key("debug").MustBool(false)
|
||||
// then from flag
|
||||
if *debug {
|
||||
debugMode = true
|
||||
}
|
||||
ctx.version = ctx.config.Section("jellyfin").Key("version").String()
|
||||
|
||||
debugMode = ctx.config.Section("ui").Key("debug").MustBool(true)
|
||||
if debugMode {
|
||||
ctx.debug = log.New(os.Stdout, "[DEBUG] ", log.Ltime|log.Lshortfile)
|
||||
app.info.Println("WARNING: Don't use debug mode in production, as it exposes pprof on the network.")
|
||||
app.debug = log.New(os.Stdout, "[DEBUG] ", log.Ltime|log.Lshortfile)
|
||||
} else {
|
||||
ctx.debug = log.New(ioutil.Discard, "", 0)
|
||||
app.debug = log.New(ioutil.Discard, "", 0)
|
||||
}
|
||||
|
||||
if !firstRun {
|
||||
ctx.host = ctx.config.Section("ui").Key("host").String()
|
||||
ctx.port = ctx.config.Section("ui").Key("port").MustInt(8056)
|
||||
app.host = app.config.Section("ui").Key("host").String()
|
||||
app.port = app.config.Section("ui").Key("port").MustInt(8056)
|
||||
|
||||
if *host != ctx.host && *host != "" {
|
||||
ctx.host = *host
|
||||
if *host != app.host && *host != "" {
|
||||
app.host = *host
|
||||
}
|
||||
if *port != ctx.port && *port > 0 {
|
||||
ctx.port = *port
|
||||
if *port != app.port && *port > 0 {
|
||||
app.port = *port
|
||||
}
|
||||
|
||||
if h := os.Getenv("JFA_HOST"); h != "" {
|
||||
ctx.host = h
|
||||
app.host = h
|
||||
if p := os.Getenv("JFA_PORT"); p != "" {
|
||||
var port int
|
||||
_, err := fmt.Sscan(p, &port)
|
||||
if err == nil {
|
||||
ctx.port = port
|
||||
app.port = port
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
address = fmt.Sprintf("%s:%d", ctx.host, ctx.port)
|
||||
address = fmt.Sprintf("%s:%d", app.host, app.port)
|
||||
|
||||
ctx.debug.Printf("Loaded config file \"%s\"", ctx.config_path)
|
||||
app.debug.Printf("Loaded config file \"%s\"", app.config_path)
|
||||
|
||||
if ctx.config.Section("ui").Key("bs5").MustBool(false) {
|
||||
ctx.cssFile = "bs5-jf.css"
|
||||
ctx.bsVersion = 5
|
||||
if app.config.Section("ui").Key("bs5").MustBool(false) {
|
||||
app.cssFile = "bs5-jf.css"
|
||||
app.bsVersion = 5
|
||||
} else {
|
||||
ctx.cssFile = "bs4-jf.css"
|
||||
ctx.bsVersion = 4
|
||||
app.cssFile = "bs4-jf.css"
|
||||
app.bsVersion = 4
|
||||
}
|
||||
|
||||
ctx.debug.Println("Loading storage")
|
||||
app.debug.Println("Loading storage")
|
||||
|
||||
ctx.storage.invite_path = filepath.Join(ctx.data_path, "invites.json")
|
||||
ctx.storage.loadInvites()
|
||||
ctx.storage.emails_path = filepath.Join(ctx.data_path, "emails.json")
|
||||
ctx.storage.loadEmails()
|
||||
ctx.storage.policy_path = filepath.Join(ctx.data_path, "user_template.json")
|
||||
ctx.storage.loadPolicy()
|
||||
ctx.storage.configuration_path = filepath.Join(ctx.data_path, "user_configuration.json")
|
||||
ctx.storage.loadConfiguration()
|
||||
ctx.storage.displayprefs_path = filepath.Join(ctx.data_path, "user_displayprefs.json")
|
||||
ctx.storage.loadDisplayprefs()
|
||||
app.storage.invite_path = filepath.Join(app.data_path, "invites.json")
|
||||
app.storage.loadInvites()
|
||||
app.storage.emails_path = filepath.Join(app.data_path, "emails.json")
|
||||
app.storage.loadEmails()
|
||||
app.storage.policy_path = filepath.Join(app.data_path, "user_template.json")
|
||||
app.storage.loadPolicy()
|
||||
app.storage.configuration_path = filepath.Join(app.data_path, "user_configuration.json")
|
||||
app.storage.loadConfiguration()
|
||||
app.storage.displayprefs_path = filepath.Join(app.data_path, "user_displayprefs.json")
|
||||
app.storage.loadDisplayprefs()
|
||||
|
||||
ctx.configBase_path = filepath.Join(ctx.local_path, "config-base.json")
|
||||
config_base, _ := ioutil.ReadFile(ctx.configBase_path)
|
||||
json.Unmarshal(config_base, &ctx.configBase)
|
||||
app.configBase_path = filepath.Join(app.local_path, "config-base.json")
|
||||
config_base, _ := ioutil.ReadFile(app.configBase_path)
|
||||
json.Unmarshal(config_base, &app.configBase)
|
||||
|
||||
themes := map[string]string{
|
||||
"Jellyfin (Dark)": fmt.Sprintf("bs%d-jf.css", ctx.bsVersion),
|
||||
"Bootstrap (Light)": fmt.Sprintf("bs%d.css", ctx.bsVersion),
|
||||
"Jellyfin (Dark)": fmt.Sprintf("bs%d-jf.css", app.bsVersion),
|
||||
"Bootstrap (Light)": fmt.Sprintf("bs%d.css", app.bsVersion),
|
||||
"Custom CSS": "",
|
||||
}
|
||||
if val, ok := themes[ctx.config.Section("ui").Key("theme").String()]; ok {
|
||||
ctx.cssFile = val
|
||||
if val, ok := themes[app.config.Section("ui").Key("theme").String()]; ok {
|
||||
app.cssFile = val
|
||||
}
|
||||
ctx.debug.Printf("Using css file \"%s\"", ctx.cssFile)
|
||||
app.debug.Printf("Using css file \"%s\"", app.cssFile)
|
||||
secret, err := GenerateSecret(16)
|
||||
if err != nil {
|
||||
ctx.err.Fatal(err)
|
||||
app.err.Fatal(err)
|
||||
}
|
||||
os.Setenv("JFA_SECRET", secret)
|
||||
ctx.jellyfinLogin = true
|
||||
if val, _ := ctx.config.Section("ui").Key("jellyfin_login").Bool(); !val {
|
||||
ctx.jellyfinLogin = false
|
||||
app.jellyfinLogin = true
|
||||
if val, _ := app.config.Section("ui").Key("jellyfin_login").Bool(); !val {
|
||||
app.jellyfinLogin = false
|
||||
user := User{}
|
||||
user.UserID = shortuuid.New()
|
||||
user.Username = ctx.config.Section("ui").Key("username").String()
|
||||
user.Password = ctx.config.Section("ui").Key("password").String()
|
||||
ctx.users = append(ctx.users, user)
|
||||
user.Username = app.config.Section("ui").Key("username").String()
|
||||
user.Password = app.config.Section("ui").Key("password").String()
|
||||
app.users = append(app.users, user)
|
||||
} else {
|
||||
ctx.debug.Println("Using Jellyfin for authentication")
|
||||
app.debug.Println("Using Jellyfin for authentication")
|
||||
}
|
||||
|
||||
server := ctx.config.Section("jellyfin").Key("server").String()
|
||||
ctx.jf.init(server, "jfa-go", ctx.version, "hrfee-arch", "hrfee-arch")
|
||||
server := app.config.Section("jellyfin").Key("server").String()
|
||||
app.jf, _ = newJellyfin(server, "jfa-go", app.version, "hrfee-arch", "hrfee-arch")
|
||||
var status int
|
||||
_, status, err = ctx.jf.authenticate(ctx.config.Section("jellyfin").Key("username").String(), ctx.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 {
|
||||
ctx.err.Fatalf("Failed to authenticate with Jellyfin @ %s: Code %d", server, status)
|
||||
app.err.Fatalf("Failed to authenticate with Jellyfin @ %s: Code %d", server, status)
|
||||
}
|
||||
ctx.info.Printf("Authenticated with %s", server)
|
||||
ctx.authJf.init(server, "jfa-go", ctx.version, "auth", "auth")
|
||||
app.info.Printf("Authenticated with %s", server)
|
||||
app.authJf, _ = newJellyfin(server, "jfa-go", app.version, "auth", "auth")
|
||||
|
||||
ctx.loadStrftime()
|
||||
app.loadStrftime()
|
||||
|
||||
validatorConf := ValidatorConf{
|
||||
"characters": ctx.config.Section("password_validation").Key("min_length").MustInt(0),
|
||||
"uppercase characters": ctx.config.Section("password_validation").Key("upper").MustInt(0),
|
||||
"lowercase characters": ctx.config.Section("password_validation").Key("lower").MustInt(0),
|
||||
"numbers": ctx.config.Section("password_validation").Key("number").MustInt(0),
|
||||
"special characters": ctx.config.Section("password_validation").Key("special").MustInt(0),
|
||||
"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),
|
||||
}
|
||||
if !ctx.config.Section("password_validation").Key("enabled").MustBool(false) {
|
||||
if !app.config.Section("password_validation").Key("enabled").MustBool(false) {
|
||||
for key := range validatorConf {
|
||||
validatorConf[key] = 0
|
||||
}
|
||||
}
|
||||
ctx.validator.init(validatorConf)
|
||||
app.validator.init(validatorConf)
|
||||
|
||||
ctx.email.init(ctx)
|
||||
app.email.init(app)
|
||||
|
||||
inviteDaemon := NewRepeater(time.Duration(60*time.Second), ctx)
|
||||
inviteDaemon := NewRepeater(time.Duration(60*time.Second), app)
|
||||
go inviteDaemon.Run()
|
||||
|
||||
if ctx.config.Section("password_resets").Key("enabled").MustBool(false) {
|
||||
go ctx.StartPWR()
|
||||
if app.config.Section("password_resets").Key("enabled").MustBool(false) {
|
||||
go app.StartPWR()
|
||||
}
|
||||
} else {
|
||||
debugMode = false
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
address = "0.0.0.0:8056"
|
||||
}
|
||||
|
||||
ctx.info.Println("Loading routes")
|
||||
app.info.Println("Loading routes")
|
||||
if debugMode {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
} else {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
router := gin.New()
|
||||
|
||||
setGinLogger(router, debugMode)
|
||||
|
||||
router.Use(gin.Recovery())
|
||||
router.Use(static.Serve("/", static.LocalFile("data/static", false)))
|
||||
router.LoadHTMLGlob("data/templates/*")
|
||||
router.NoRoute(ctx.NoRouteHandler)
|
||||
router.Use(static.Serve("/", static.LocalFile(filepath.Join(app.local_path, "static"), false)))
|
||||
router.LoadHTMLGlob(filepath.Join(app.local_path, "templates", "*"))
|
||||
router.NoRoute(app.NoRouteHandler)
|
||||
if debugMode {
|
||||
app.debug.Println("Loading pprof")
|
||||
pprof.Register(router)
|
||||
}
|
||||
if !firstRun {
|
||||
router.GET("/", ctx.AdminPage)
|
||||
router.GET("/getToken", ctx.GetToken)
|
||||
router.POST("/newUser", ctx.NewUser)
|
||||
router.GET("/invite/:invCode", ctx.InviteProxy)
|
||||
router.Use(static.Serve("/invite/", static.LocalFile("data/static", false)))
|
||||
api := router.Group("/", ctx.webAuth())
|
||||
api.POST("/generateInvite", ctx.GenerateInvite)
|
||||
api.GET("/getInvites", ctx.GetInvites)
|
||||
api.POST("/setNotify", ctx.SetNotify)
|
||||
api.POST("/deleteInvite", ctx.DeleteInvite)
|
||||
api.GET("/getUsers", ctx.GetUsers)
|
||||
api.POST("/modifyUsers", ctx.ModifyEmails)
|
||||
api.POST("/setDefaults", ctx.SetDefaults)
|
||||
api.GET("/getConfig", ctx.GetConfig)
|
||||
api.POST("/modifyConfig", ctx.ModifyConfig)
|
||||
ctx.info.Printf("Starting router @ %s", address)
|
||||
router.GET("/", app.AdminPage)
|
||||
router.GET("/getToken", app.getToken)
|
||||
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)
|
||||
api := router.Group("/", app.webAuth())
|
||||
router.POST("/logout", app.Logout)
|
||||
api.POST("/generateInvite", app.GenerateInvite)
|
||||
api.GET("/getInvites", app.GetInvites)
|
||||
api.POST("/setNotify", app.SetNotify)
|
||||
api.POST("/deleteInvite", app.DeleteInvite)
|
||||
api.GET("/getUsers", app.GetUsers)
|
||||
api.POST("/modifyUsers", app.ModifyEmails)
|
||||
api.POST("/setDefaults", app.SetDefaults)
|
||||
api.GET("/getConfig", app.GetConfig)
|
||||
api.POST("/modifyConfig", app.ModifyConfig)
|
||||
app.info.Printf("Starting router @ %s", address)
|
||||
} else {
|
||||
router.GET("/", func(gc *gin.Context) {
|
||||
gc.HTML(200, "setup.html", gin.H{})
|
||||
})
|
||||
router.POST("/testJF", ctx.TestJF)
|
||||
router.POST("/modifyConfig", ctx.ModifyConfig)
|
||||
ctx.info.Printf("Loading setup @ %s", address)
|
||||
router.POST("/testJF", app.TestJF)
|
||||
router.POST("/modifyConfig", app.ModifyConfig)
|
||||
app.info.Printf("Loading setup @ %s", address)
|
||||
}
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: address,
|
||||
Handler: router,
|
||||
}
|
||||
go func() {
|
||||
if err := srv.ListenAndServe(); err != nil {
|
||||
app.err.Printf("Failure serving: %s", err)
|
||||
}
|
||||
}()
|
||||
app.quit = make(chan os.Signal)
|
||||
signal.Notify(app.quit, os.Interrupt)
|
||||
<-app.quit
|
||||
app.info.Println("Shutting down...")
|
||||
|
||||
cntx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
if err := srv.Shutdown(cntx); err != nil {
|
||||
app.err.Fatalf("Server shutdown error: %s", err)
|
||||
}
|
||||
router.Run(address)
|
||||
}
|
||||
|
||||
47
pwreset.go
@@ -2,33 +2,34 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
func (ctx *appContext) StartPWR() {
|
||||
ctx.info.Println("Starting password reset daemon")
|
||||
path := ctx.config.Section("password_resets").Key("watch_directory").String()
|
||||
func (app *appContext) StartPWR() {
|
||||
app.info.Println("Starting password reset daemon")
|
||||
path := app.config.Section("password_resets").Key("watch_directory").String()
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
ctx.err.Printf("Failed to start password reset daemon: Directory \"%s\" doesn't exist", path)
|
||||
app.err.Printf("Failed to start password reset daemon: Directory \"%s\" doesn't exist", path)
|
||||
return
|
||||
}
|
||||
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
ctx.err.Printf("Couldn't initialise password reset daemon")
|
||||
app.err.Printf("Couldn't initialise password reset daemon")
|
||||
return
|
||||
}
|
||||
defer watcher.Close()
|
||||
|
||||
done := make(chan bool)
|
||||
go pwrMonitor(ctx, watcher)
|
||||
go pwrMonitor(app, watcher)
|
||||
err = watcher.Add(path)
|
||||
if err != nil {
|
||||
ctx.err.Printf("Failed to start password reset daemon: %s", err)
|
||||
app.err.Printf("Failed to start password reset daemon: %s", err)
|
||||
}
|
||||
<-done
|
||||
}
|
||||
@@ -39,7 +40,7 @@ type Pwr struct {
|
||||
Expiry time.Time `json:"ExpirationDate"`
|
||||
}
|
||||
|
||||
func pwrMonitor(ctx *appContext, watcher *fsnotify.Watcher) {
|
||||
func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) {
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-watcher.Events:
|
||||
@@ -56,29 +57,29 @@ func pwrMonitor(ctx *appContext, watcher *fsnotify.Watcher) {
|
||||
if len(pwr.Pin) == 0 || err != nil {
|
||||
return
|
||||
}
|
||||
ctx.info.Printf("New password reset for user \"%s\"", pwr.Username)
|
||||
app.info.Printf("New password reset for user \"%s\"", pwr.Username)
|
||||
if ct := time.Now(); pwr.Expiry.After(ct) {
|
||||
user, status, err := ctx.jf.userByName(pwr.Username, false)
|
||||
user, status, err := app.jf.userByName(pwr.Username, false)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
ctx.err.Printf("Failed to get users from Jellyfin: Code %d", status)
|
||||
ctx.debug.Printf("Error: %s", err)
|
||||
app.err.Printf("Failed to get users from Jellyfin: Code %d", status)
|
||||
app.debug.Printf("Error: %s", err)
|
||||
return
|
||||
}
|
||||
ctx.storage.loadEmails()
|
||||
address, ok := ctx.storage.emails[user["Id"].(string)].(string)
|
||||
app.storage.loadEmails()
|
||||
address, ok := app.storage.emails[user["Id"].(string)].(string)
|
||||
if !ok {
|
||||
ctx.err.Printf("Couldn't find email for user \"%s\". Make sure it's set", pwr.Username)
|
||||
app.err.Printf("Couldn't find email for user \"%s\". Make sure it's set", pwr.Username)
|
||||
return
|
||||
}
|
||||
if ctx.email.constructReset(pwr, ctx) != nil {
|
||||
ctx.err.Printf("Failed to construct password reset email for %s", pwr.Username)
|
||||
} else if ctx.email.send(address, ctx) != nil {
|
||||
ctx.err.Printf("Failed to send password reset email to \"%s\"", address)
|
||||
if app.email.constructReset(pwr, app) != nil {
|
||||
app.err.Printf("Failed to construct password reset email for %s", pwr.Username)
|
||||
} else if app.email.send(address, app) != nil {
|
||||
app.err.Printf("Failed to send password reset email to \"%s\"", address)
|
||||
} else {
|
||||
ctx.info.Printf("Sent password reset email to \"%s\"", address)
|
||||
app.info.Printf("Sent password reset email to \"%s\"", address)
|
||||
}
|
||||
} else {
|
||||
ctx.err.Printf("Password reset for user \"%s\" has already expired (%s). Check your time settings.", pwr.Username, pwr.Expiry)
|
||||
app.err.Printf("Password reset for user \"%s\" has already expired (%s). Check your time settings.", pwr.Username, pwr.Expiry)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -86,7 +87,7 @@ func pwrMonitor(ctx *appContext, watcher *fsnotify.Watcher) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
ctx.err.Printf("Password reset daemon: %s", err)
|
||||
app.err.Printf("Password reset daemon: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,34 +2,81 @@
|
||||
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":
|
||||
return subprocess.check_output(cmd, shell=True)
|
||||
proc = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE)
|
||||
return proc.communicate()
|
||||
|
||||
|
||||
local_path = Path(__file__).resolve().parent
|
||||
node_bin = local_path.parent / 'node_modules' / '.bin'
|
||||
out = runcmd("npm bin")
|
||||
|
||||
for bsv in [d for d in local_path.iterdir() if 'bs' in d.name]:
|
||||
scss = bsv / f'{bsv.name}-jf.scss'
|
||||
css = bsv / f'{bsv.name}-jf.css'
|
||||
min_css = bsv.parents[1] / 'data' / 'static' / f'{bsv.name}-jf.css'
|
||||
with open(css, 'w') as f:
|
||||
f.write(sass.compile(filename=str(scss.resolve()),
|
||||
output_style='expanded',
|
||||
precision=6))
|
||||
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"
|
||||
css = bsv / f"{bsv.name}-jf.css"
|
||||
min_css = bsv.parents[1] / "data" / "static" / f"{bsv.name}-jf.css"
|
||||
with open(css, "w") as f:
|
||||
f.write(
|
||||
sass.compile(
|
||||
filename=str(scss.resolve()), output_style="expanded", precision=6
|
||||
)
|
||||
)
|
||||
if css.exists():
|
||||
print(f'{bsv.name}: Compiled.')
|
||||
runcmd(f'{str((node_bin / "postcss").resolve())} {str(css.resolve())} --replace --use autoprefixer')
|
||||
print(f'{bsv.name}: Prefixed.')
|
||||
runcmd(f'{str((node_bin / "cleancss").resolve())} --level 1 --format breakWith=lf --output {str(min_css.resolve())} {str(css.resolve())}')
|
||||
print(f"{bsv.name}: Compiled.")
|
||||
# postcss only excepts forwards slashes? weird.
|
||||
cssPath = str(css.resolve())
|
||||
if os.name == "nt":
|
||||
cssPath = cssPath.replace("\\", "/")
|
||||
runcmd(
|
||||
f'{str((node_bin / "postcss").resolve())} {cssPath} --replace --use autoprefixer'
|
||||
)
|
||||
print(f"{bsv.name}: Prefixed.")
|
||||
runcmd(
|
||||
f'{str((node_bin / "cleancss").resolve())} --level 1 --format breakWith=lf --output {str(min_css.resolve())} {str(css.resolve())}'
|
||||
)
|
||||
if min_css.exists():
|
||||
print(f'{bsv.name}: Minified and copied to {str(min_css.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"{bsv.name}: Minified and copied to {str(min_css.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}")
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
#!/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]
|
||||
runcmd(f'npm install --prefix {root_path}')
|
||||
if os.name == 'nt':
|
||||
root_path /= 'node_modules'
|
||||
|
||||
if (root_path / 'node_modules' / 'cleancss').exists():
|
||||
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())}.')
|
||||
|
||||
|
||||
|
||||
8
setup.go
@@ -10,14 +10,14 @@ type testReq struct {
|
||||
Password string `json:"jfPassword"`
|
||||
}
|
||||
|
||||
func (ctx *appContext) TestJF(gc *gin.Context) {
|
||||
func (app *appContext) TestJF(gc *gin.Context) {
|
||||
var req testReq
|
||||
gc.BindJSON(&req)
|
||||
tempjf := Jellyfin{}
|
||||
tempjf.init(req.Host, "jfa-go-setup", ctx.version, "auth", "auth")
|
||||
tempjf, _ := newJellyfin(req.Host, "jfa-go-setup", app.version, "auth", "auth")
|
||||
tempjf.noFail = true
|
||||
_, status, err := tempjf.authenticate(req.Username, req.Password)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
ctx.info.Printf("Auth failed with code %d (%s)", status, err)
|
||||
app.info.Printf("Auth failed with code %d (%s)", status, err)
|
||||
gc.JSON(401, map[string]bool{"success": false})
|
||||
return
|
||||
}
|
||||
|
||||
53
views.go
@@ -1,54 +1,55 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (ctx *appContext) AdminPage(gc *gin.Context) {
|
||||
bs5 := ctx.config.Section("ui").Key("bs5").MustBool(false)
|
||||
emailEnabled, _ := ctx.config.Section("invite_emails").Key("enabled").Bool()
|
||||
notificationsEnabled, _ := ctx.config.Section("notifications").Key("enabled").Bool()
|
||||
func (app *appContext) AdminPage(gc *gin.Context) {
|
||||
bs5 := app.config.Section("ui").Key("bs5").MustBool(false)
|
||||
emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool()
|
||||
notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool()
|
||||
gc.HTML(http.StatusOK, "admin.html", gin.H{
|
||||
"bs5": bs5,
|
||||
"cssFile": ctx.cssFile,
|
||||
"cssFile": app.cssFile,
|
||||
"contactMessage": "",
|
||||
"email_enabled": emailEnabled,
|
||||
"notifications": notificationsEnabled,
|
||||
})
|
||||
}
|
||||
|
||||
func (ctx *appContext) InviteProxy(gc *gin.Context) {
|
||||
func (app *appContext) InviteProxy(gc *gin.Context) {
|
||||
code := gc.Param("invCode")
|
||||
/* Don't actually check if the invite is valid, just if it exists, just so the page loads quicker. Invite is actually checked on submit anyway. */
|
||||
// if ctx.checkInvite(code, false, "") {
|
||||
if _, ok := ctx.storage.invites[code]; ok {
|
||||
email := ctx.storage.invites[code].Email
|
||||
// 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": ctx.config.Section("ui").Key("bs5").MustBool(false),
|
||||
"cssFile": ctx.cssFile,
|
||||
"contactMessage": ctx.config.Section("ui").Key("contac_message").String(),
|
||||
"helpMessage": ctx.config.Section("ui").Key("help_message").String(),
|
||||
"successMessage": ctx.config.Section("ui").Key("success_message").String(),
|
||||
"jfLink": ctx.config.Section("jellyfin").Key("public_server").String(),
|
||||
"validate": ctx.config.Section("password_validation").Key("enabled").MustBool(false),
|
||||
"requirements": ctx.validator.getCriteria(),
|
||||
"bs5": app.config.Section("ui").Key("bs5").MustBool(false),
|
||||
"cssFile": app.cssFile,
|
||||
"contactMessage": app.config.Section("ui").Key("contac_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": !ctx.config.Section("email").Key("no_username").MustBool(false),
|
||||
"username": !app.config.Section("email").Key("no_username").MustBool(false),
|
||||
})
|
||||
} else {
|
||||
gc.HTML(404, "invalidCode.html", gin.H{
|
||||
"bs5": ctx.config.Section("ui").Key("bs5").MustBool(false),
|
||||
"cssFile": ctx.cssFile,
|
||||
"contactMessage": ctx.config.Section("ui").Key("contac_message").String(),
|
||||
"bs5": app.config.Section("ui").Key("bs5").MustBool(false),
|
||||
"cssFile": app.cssFile,
|
||||
"contactMessage": app.config.Section("ui").Key("contac_message").String(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *appContext) NoRouteHandler(gc *gin.Context) {
|
||||
func (app *appContext) NoRouteHandler(gc *gin.Context) {
|
||||
gc.HTML(404, "404.html", gin.H{
|
||||
"bs5": ctx.config.Section("ui").Key("bs5").MustBool(false),
|
||||
"cssFile": ctx.cssFile,
|
||||
"contactMessage": ctx.config.Section("ui").Key("contact_message").String(),
|
||||
"bs5": app.config.Section("ui").Key("bs5").MustBool(false),
|
||||
"cssFile": app.cssFile,
|
||||
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
||||
})
|
||||
}
|
||||
|
||||