mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-29 12:30:11 +00:00
Compare commits
No commits in common. "6308db495a0aee206d2d9cd6047183ec9901291e" and "86c37fb4237bc645e5fcb4a52f5908ae23a9f154" have entirely different histories.
6308db495a
...
86c37fb423
@ -8,8 +8,46 @@ release:
|
|||||||
name_template: "v{{.Version}}"
|
name_template: "v{{.Version}}"
|
||||||
before:
|
before:
|
||||||
hooks:
|
hooks:
|
||||||
- npm i
|
- go mod download
|
||||||
- make precompile INTERNAL=on
|
- rm -rf data/web
|
||||||
|
- mkdir -p data/web/css
|
||||||
|
- cp images/banner.svg static/banner.svg
|
||||||
|
- bash -c 'cp -r static/* data/web/'
|
||||||
|
- npm install
|
||||||
|
- npm install esbuild
|
||||||
|
- cp node_modules/remixicon/fonts/remixicon.css node_modules/remixicon/fonts/remixicon.woff2 data/web/css/
|
||||||
|
- cp -r html data/
|
||||||
|
- node scripts/missing-colors.js html data/html
|
||||||
|
- cp -r lang data/
|
||||||
|
- cp LICENSE data/
|
||||||
|
- cp jfa-go.service data/
|
||||||
|
- python3 scripts/enumerate_config.py -i config/config-base.json -o data/config-base.json
|
||||||
|
- python3 scripts/generate_ini.py -i config/config-base.json -o data/config-default.ini
|
||||||
|
- python3 scripts/compile_mjml.py -o data/
|
||||||
|
- rm -rf tempts
|
||||||
|
- cp -r ts tempts
|
||||||
|
- scripts/dark-variant.sh tempts
|
||||||
|
- scripts/dark-variant.sh tempts/modules
|
||||||
|
- mkdir -p data/web/js
|
||||||
|
- npx esbuild --target=es6 --bundle tempts/admin.ts {{.Env.JFA_GO_SOURCEMAP}} --outfile=./data/web/js/admin.js {{.Env.JFA_GO_MINIFY}}
|
||||||
|
- npx esbuild --target=es6 --bundle tempts/user.ts {{.Env.JFA_GO_SOURCEMAP}} --outfile=./data/web/js/user.js {{.Env.JFA_GO_MINIFY}}
|
||||||
|
- npx esbuild --target=es6 --bundle tempts/pwr.ts {{.Env.JFA_GO_SOURCEMAP}} --outfile=./data/web/js/pwr.js {{.Env.JFA_GO_MINIFY}}
|
||||||
|
- npx esbuild --target=es6 --bundle tempts/pwr-pin.ts {{.Env.JFA_GO_SOURCEMAP}} --outfile=./data/web/js/pwr-pin.js {{.Env.JFA_GO_MINIFY}}
|
||||||
|
- npx esbuild --target=es6 --bundle tempts/form.ts {{.Env.JFA_GO_SOURCEMAP}} --outfile=./data/web/js/form.js {{.Env.JFA_GO_MINIFY}}
|
||||||
|
- npx esbuild --target=es6 --bundle tempts/setup.ts {{.Env.JFA_GO_SOURCEMAP}} --outfile=./data/web/js/setup.js {{.Env.JFA_GO_MINIFY}}
|
||||||
|
- npx esbuild --target=es6 --bundle tempts/crash.ts {{.Env.JFA_GO_SOURCEMAP}} --outfile=./data/crash.js {{.Env.JFA_GO_MINIFY}}
|
||||||
|
- bash -c "{{.Env.JFA_GO_COPYTS}}"
|
||||||
|
- rm -r tempts
|
||||||
|
- npx esbuild --bundle css/base.css --outfile=./data/web/css/bundle.css --external:remixicon.css --external:../fonts/hanken* --minify
|
||||||
|
- cp html/crash.html data/
|
||||||
|
- npx tailwindcss -i data/web/css/bundle.css -o data/bundle.css --content "html/crash.html"
|
||||||
|
- node scripts/inline.js root data data/crash.html data/crash.html
|
||||||
|
- rm data/bundle.css
|
||||||
|
- npx tailwindcss -i data/web/css/bundle.css -o data/web/css/bundle.css
|
||||||
|
- mv data/crash.html data/html/
|
||||||
|
- go install github.com/swaggo/swag/cmd/swag@latest
|
||||||
|
- swag init -g main.go
|
||||||
|
- mv data/web/css/bundle.css data/web/css/{{.Env.JFA_GO_CSS_VERSION}}bundle.css
|
||||||
builds:
|
builds:
|
||||||
- id: notray
|
- id: notray
|
||||||
dir: ./
|
dir: ./
|
||||||
|
@ -12,6 +12,14 @@ clone:
|
|||||||
depth: 0
|
depth: 0
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: redoc
|
||||||
|
image: docker.io/hrfee/jfa-go-build-docker:latest
|
||||||
|
environment:
|
||||||
|
REDOC_SSH_ID:
|
||||||
|
from_secret: REDOC_SSH_ID
|
||||||
|
commands:
|
||||||
|
- sh -c "echo \"$REDOC_SSH_ID\" > /tmp/id_redoc && chmod 600 /tmp/id_redoc"
|
||||||
|
- bash -c 'sftp -P 3625 -i /tmp/id_redoc -o StrictHostKeyChecking=no redoc@api.jfa-go.com:/home/redoc <<< $"put docs/swagger.json jfa-go.json"'
|
||||||
- name: build
|
- name: build
|
||||||
image: docker.io/hrfee/jfa-go-build-docker:latest
|
image: docker.io/hrfee/jfa-go-build-docker:latest
|
||||||
environment:
|
environment:
|
||||||
@ -22,14 +30,6 @@ steps:
|
|||||||
- curl -sfL https://goreleaser.com/static/run > goreleaser
|
- curl -sfL https://goreleaser.com/static/run > goreleaser
|
||||||
- chmod +x goreleaser
|
- chmod +x goreleaser
|
||||||
- ./scripts/version.sh ./goreleaser --snapshot --skip=publish --clean
|
- ./scripts/version.sh ./goreleaser --snapshot --skip=publish --clean
|
||||||
- name: redoc
|
|
||||||
image: docker.io/hrfee/jfa-go-build-docker:latest
|
|
||||||
environment:
|
|
||||||
REDOC_SSH_ID:
|
|
||||||
from_secret: REDOC_SSH_ID
|
|
||||||
commands:
|
|
||||||
- sh -c "echo \"$REDOC_SSH_ID\" > /tmp/id_redoc && chmod 600 /tmp/id_redoc"
|
|
||||||
- bash -c 'sftp -P 3625 -i /tmp/id_redoc -o StrictHostKeyChecking=no redoc@api.jfa-go.com:/home/redoc <<< $"put docs/swagger.json jfa-go.json"'
|
|
||||||
- name: deb-repo
|
- name: deb-repo
|
||||||
image: docker.io/hrfee/jfa-go-build-docker:latest
|
image: docker.io/hrfee/jfa-go-build-docker:latest
|
||||||
environment:
|
environment:
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Use this instead if hrfee/jfa-go-build-docker doesn't support your architecture
|
# Use this instead if hrfee/jfa-go-build-docker doesn't support your architecture
|
||||||
# FROM --platform=$BUILDPLATFORM golang:latest AS support
|
# FROM --platform=$BUILDPLATFORM golang:latest AS support
|
||||||
FROM --platform=$BUILDPLATFORM docker.io/hrfee/jfa-go-build-docker:latest AS support
|
FROM --platform=$BUILDPLATFORM hrfee/jfa-go-build-docker AS support
|
||||||
|
|
||||||
COPY . /opt/build
|
COPY . /opt/build
|
||||||
|
|
||||||
@ -9,10 +9,10 @@ COPY . /opt/build
|
|||||||
# && apt-get install build-essential python3-pip -y \
|
# && apt-get install build-essential python3-pip -y \
|
||||||
# && (curl -sL https://deb.nodesource.com/setup_current.x | bash -) \
|
# && (curl -sL https://deb.nodesource.com/setup_current.x | bash -) \
|
||||||
# && apt-get install nodejs
|
# && apt-get install nodejs
|
||||||
RUN (cd /opt/build; npm i; make precompile INTERNAL=off GOESBUILD=off) \
|
RUN (cd /opt/build; make configuration npm email typescript variants-html bundle-css inline-css swagger copy INTERNAL=off GOESBUILD=on) \
|
||||||
&& sed -i 's#id="password_resets-watch_directory" placeholder="/config/jellyfin"#id="password_resets-watch_directory" value="/jf" disabled#g' /opt/build/build/data/html/setup.html
|
&& sed -i 's#id="password_resets-watch_directory" placeholder="/config/jellyfin"#id="password_resets-watch_directory" value="/jf" disabled#g' /opt/build/build/data/html/setup.html
|
||||||
|
|
||||||
FROM --platform=$BUILDPLATFORM docker.io/golang:latest AS build
|
FROM --platform=$BUILDPLATFORM golang:latest AS build
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ENV GOARCH=$TARGETARCH
|
ENV GOARCH=$TARGETARCH
|
||||||
ARG BUILT_BY
|
ARG BUILT_BY
|
||||||
@ -30,3 +30,5 @@ EXPOSE 8056
|
|||||||
EXPOSE 8057
|
EXPOSE 8057
|
||||||
|
|
||||||
CMD [ "/opt/jfa-go/jfa-go", "-data", "/data" ]
|
CMD [ "/opt/jfa-go/jfa-go", "-data", "/data" ]
|
||||||
|
|
||||||
|
|
||||||
|
145
Makefile
145
Makefile
@ -1,7 +1,3 @@
|
|||||||
.PHONY: configuration email typescript swagger copy compile compress tailwind bundle-css inline-css variants-html install clean npm config-description config-default precompile
|
|
||||||
|
|
||||||
all: compile
|
|
||||||
|
|
||||||
GOESBUILD ?= off
|
GOESBUILD ?= off
|
||||||
ifeq ($(GOESBUILD), on)
|
ifeq ($(GOESBUILD), on)
|
||||||
ESBUILD := esbuild
|
ESBUILD := esbuild
|
||||||
@ -11,7 +7,6 @@ endif
|
|||||||
GOBINARY ?= go
|
GOBINARY ?= go
|
||||||
|
|
||||||
CSSVERSION ?= v3
|
CSSVERSION ?= v3
|
||||||
CSS_BUNDLE = $(DATA)/web/css/$(CSSVERSION)bundle.css
|
|
||||||
|
|
||||||
VERSION ?= $(shell git describe --exact-match HEAD 2> /dev/null || echo vgit)
|
VERSION ?= $(shell git describe --exact-match HEAD 2> /dev/null || echo vgit)
|
||||||
VERSION := $(shell echo $(VERSION) | sed 's/v//g')
|
VERSION := $(shell echo $(VERSION) | sed 's/v//g')
|
||||||
@ -62,7 +57,7 @@ ifeq ($(DEBUG), on)
|
|||||||
TYPECHECK := npx tsc -noEmit --project ts/tsconfig.json
|
TYPECHECK := npx tsc -noEmit --project ts/tsconfig.json
|
||||||
# jank
|
# jank
|
||||||
COPYTS := rm -r $(DATA)/web/js/ts; cp -r tempts $(DATA)/web/js/ts
|
COPYTS := rm -r $(DATA)/web/js/ts; cp -r tempts $(DATA)/web/js/ts
|
||||||
UNCSS := cp $(CSS_BUNDLE) $(DATA)/bundle.css
|
UNCSS := cp $(DATA)/web/css/bundle.css $(DATA)/bundle.css
|
||||||
# TAILWIND := --content ""
|
# TAILWIND := --content ""
|
||||||
else
|
else
|
||||||
LDFLAGS := -s -w $(LDFLAGS)
|
LDFLAGS := -s -w $(LDFLAGS)
|
||||||
@ -70,7 +65,7 @@ else
|
|||||||
MINIFY := --minify
|
MINIFY := --minify
|
||||||
COPYTS :=
|
COPYTS :=
|
||||||
TYPECHECK :=
|
TYPECHECK :=
|
||||||
UNCSS := npx tailwindcss -i $(CSS_BUNDLE) -o $(DATA)/bundle.css --content "html/crash.html"
|
UNCSS := npx tailwindcss -i $(DATA)/web/css/bundle.css -o $(DATA)/bundle.css --content "html/crash.html"
|
||||||
# UNCSS := npx uncss $(DATA)/crash.html --csspath web/css --output $(DATA)/bundle.css
|
# UNCSS := npx uncss $(DATA)/crash.html --csspath web/css --output $(DATA)/bundle.css
|
||||||
TAILWIND :=
|
TAILWIND :=
|
||||||
endif
|
endif
|
||||||
@ -101,104 +96,77 @@ else
|
|||||||
SWAGINSTALL :=
|
SWAGINSTALL :=
|
||||||
endif
|
endif
|
||||||
|
|
||||||
CONFIG_BASE = config/config-base.json
|
npm:
|
||||||
|
$(info installing npm dependencies)
|
||||||
|
npm install $(NPMOPTS)
|
||||||
|
|
||||||
CONFIG_DESCRIPTION = $(DATA)/config-base.json
|
configuration:
|
||||||
CONFIG_DEFAULT = $(DATA)/config-default.ini
|
|
||||||
$(CONFIG_DESCRIPTION) &: $(CONFIG_BASE)
|
|
||||||
$(info Fixing config-base)
|
$(info Fixing config-base)
|
||||||
-mkdir -p $(DATA)
|
-mkdir -p $(DATA)
|
||||||
python3 scripts/enumerate_config.py -i config/config-base.json -o $(DATA)/config-base.json
|
python3 scripts/enumerate_config.py -i config/config-base.json -o $(DATA)/config-base.json
|
||||||
|
|
||||||
$(CONFIG_DEFAULT) &: $(CONFIG_BASE)
|
|
||||||
$(info Generating config-default.ini)
|
$(info Generating config-default.ini)
|
||||||
python3 scripts/generate_ini.py -i config/config-base.json -o $(DATA)/config-default.ini
|
python3 scripts/generate_ini.py -i config/config-base.json -o $(DATA)/config-default.ini
|
||||||
|
|
||||||
configuration: $(CONFIG_DESCRIPTION) $(CONFIG_DEFAULT)
|
email:
|
||||||
|
|
||||||
EMAIL_SRC_MJML = $(wildcard mail/*.mjml)
|
|
||||||
EMAIL_SRC_TXT = $(wildcard mail/*.txt)
|
|
||||||
EMAIL_DATA_MJML = $(EMAIL_SRC_MJML:mail/%=data/%)
|
|
||||||
EMAIL_HTML = $(EMAIL_DATA_MJML:.mjml=.html)
|
|
||||||
EMAIL_TXT = $(EMAIL_SRC_TXT:mail/%=data/%)
|
|
||||||
EMAIL_ALL = $(EMAIL_HTML) $(EMAIL_TXT)
|
|
||||||
EMAIL_TARGET = mail/confirmation.html
|
|
||||||
$(EMAIL_TARGET): $(EMAIL_SRC_MJML) $(EMAIL_SRC_TXT)
|
|
||||||
$(info Generating email html)
|
$(info Generating email html)
|
||||||
python3 scripts/compile_mjml.py -o $(DATA)/
|
python3 scripts/compile_mjml.py -o $(DATA)/
|
||||||
|
|
||||||
TYPESCRIPT_FULLSRC = $(shell find ts/ -type f -name "*.ts")
|
typescript:
|
||||||
TYPESCRIPT_SRC = $(wildcard ts/*.ts)
|
|
||||||
TYPESCRIPT_TEMPSRC = $(TYPESCRIPT_SRC:ts/%=tempts/%)
|
|
||||||
# TYPESCRIPT_TARGET = $(patsubst %.ts,%.js,$(subst tempts/,./$(DATA)/web/js/,$(TYPESCRIPT_TEMPSRC)))
|
|
||||||
TYPESCRIPT_TARGET = $(DATA)/web/js/admin.js
|
|
||||||
$(TYPESCRIPT_TARGET): $(TYPESCRIPT_FULLSRC) ts/tsconfig.json
|
|
||||||
$(TYPECHECK)
|
$(TYPECHECK)
|
||||||
|
$(adding dark variants to typescript)
|
||||||
rm -rf tempts
|
rm -rf tempts
|
||||||
cp -r ts tempts
|
cp -r ts tempts
|
||||||
$(adding dark variants to typescript)
|
|
||||||
scripts/dark-variant.sh tempts
|
scripts/dark-variant.sh tempts
|
||||||
scripts/dark-variant.sh tempts/modules
|
scripts/dark-variant.sh tempts/modules
|
||||||
$(info compiling typescript)
|
$(info compiling typescript)
|
||||||
mkdir -p $(DATA)/web/js
|
mkdir -p $(DATA)/web/js
|
||||||
$(foreach tempsrc,$(TYPESCRIPT_TEMPSRC),$(ESBUILD) --target=es6 --bundle $(tempsrc) $(SOURCEMAP) --outfile=$(patsubst %.ts,%.js,$(subst tempts/,./$(DATA)/web/js/,$(tempsrc))) $(MINIFY);)
|
$(ESBUILD) --target=es6 --bundle tempts/admin.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/admin.js $(MINIFY)
|
||||||
mv $(DATA)/web/js/crash.js $(DATA)/
|
$(ESBUILD) --target=es6 --bundle tempts/user.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/user.js $(MINIFY)
|
||||||
|
$(ESBUILD) --target=es6 --bundle tempts/pwr.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/pwr.js $(MINIFY)
|
||||||
|
$(ESBUILD) --target=es6 --bundle tempts/pwr-pin.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/pwr-pin.js $(MINIFY)
|
||||||
|
$(ESBUILD) --target=es6 --bundle tempts/form.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/form.js $(MINIFY)
|
||||||
|
$(ESBUILD) --target=es6 --bundle tempts/setup.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/setup.js $(MINIFY)
|
||||||
|
$(ESBUILD) --target=es6 --bundle tempts/crash.ts --outfile=./$(DATA)/crash.js $(MINIFY)
|
||||||
$(COPYTS)
|
$(COPYTS)
|
||||||
|
|
||||||
SWAGGER_SRC = $(wildcard api*.go) $(wildcard *auth.go) views.go
|
swagger:
|
||||||
SWAGGER_TARGET = docs/docs.go
|
|
||||||
$(SWAGGER_TARGET): $(SWAGGER_SRC)
|
|
||||||
$(SWAGINSTALL)
|
$(SWAGINSTALL)
|
||||||
swag init -g main.go
|
swag init -g main.go
|
||||||
|
|
||||||
VARIANTS_SRC = $(wildcard html/*.html)
|
compile:
|
||||||
VARIANTS_TARGET = $(DATA)/html/admin.html
|
$(info Downloading deps)
|
||||||
$(VARIANTS_TARGET): $(VARIANTS_SRC)
|
$(GOBINARY) mod download
|
||||||
|
$(info Building)
|
||||||
|
mkdir -p build
|
||||||
|
$(GOBINARY) build $(RACEDETECTOR) -ldflags="$(LDFLAGS)" $(TAGS) -o build/jfa-go
|
||||||
|
|
||||||
|
compress:
|
||||||
|
upx --lzma build/jfa-go
|
||||||
|
|
||||||
|
bundle-css:
|
||||||
|
mkdir -p $(DATA)/web/css
|
||||||
|
$(info copying fonts)
|
||||||
|
cp -r node_modules/remixicon/fonts/remixicon.css node_modules/remixicon/fonts/remixicon.woff2 $(DATA)/web/css/
|
||||||
|
$(info bundling css)
|
||||||
|
$(ESBUILD) --bundle css/base.css --outfile=$(DATA)/web/css/bundle.css --external:remixicon.css --external:../fonts/hanken* --minify
|
||||||
|
npx tailwindcss -i $(DATA)/web/css/bundle.css -o $(DATA)/web/css/bundle.css $(TAILWIND)
|
||||||
|
# npx postcss -o $(DATA)/web/css/bundle.css $(DATA)/web/css/bundle.css
|
||||||
|
|
||||||
|
inline-css:
|
||||||
|
cp html/crash.html $(DATA)/crash.html
|
||||||
|
$(UNCSS)
|
||||||
|
node scripts/inline.js root $(DATA) $(DATA)/crash.html $(DATA)/crash.html
|
||||||
|
rm $(DATA)/bundle.css
|
||||||
|
|
||||||
|
variants-html:
|
||||||
$(info copying html)
|
$(info copying html)
|
||||||
cp -r html $(DATA)/
|
cp -r html $(DATA)/
|
||||||
$(info adding dark variants to html)
|
$(info adding dark variants to html)
|
||||||
node scripts/missing-colors.js html $(DATA)/html
|
node scripts/missing-colors.js html $(DATA)/html
|
||||||
|
|
||||||
ICON_SRC = node_modules/remixicon/fonts/remixicon.css node_modules/remixicon/fonts/remixicon.woff2
|
copy:
|
||||||
ICON_TARGET = $(ICON_SRC:node_modules/remixicon/fonts/%=$(DATA)/web/css/%)
|
|
||||||
CSS_SRC = $(wildcard css/*.css)
|
|
||||||
CSS_TARGET = $(DATA)/web/css/part-bundle.css
|
|
||||||
CSS_FULLTARGET = $(CSS_BUNDLE)
|
|
||||||
ALL_CSS_SRC = $(ICON_SRC) $(CSS_SRC)
|
|
||||||
ALL_CSS_TARGET = $(ICON_TARGET)
|
|
||||||
|
|
||||||
$(CSS_FULLTARGET): $(TYPESCRIPT_TARGET) $(VARIANTS_TARGET) $(ALL_CSS_SRC) $(wildcard html/*.html)
|
|
||||||
mkdir -p $(DATA)/web/css
|
|
||||||
$(info copying fonts)
|
|
||||||
cp -r node_modules/remixicon/fonts/remixicon.css node_modules/remixicon/fonts/remixicon.woff2 $(DATA)/web/css/
|
|
||||||
$(info bundling css)
|
|
||||||
$(ESBUILD) --bundle css/base.css --outfile=$(CSS_TARGET) --external:remixicon.css --external:../fonts/hanken* --minify
|
|
||||||
|
|
||||||
npx tailwindcss -i $(CSS_TARGET) -o $(CSS_FULLTARGET) $(TAILWIND)
|
|
||||||
rm $(CSS_TARGET)
|
|
||||||
# mv $(CSS_BUNDLE) $(DATA)/web/css/$(CSSVERSION)bundle.css
|
|
||||||
# npx postcss -o $(CSS_TARGET) $(CSS_TARGET)
|
|
||||||
|
|
||||||
bundle-css: tailwind
|
|
||||||
|
|
||||||
INLINE_SRC = html/crash.html
|
|
||||||
INLINE_TARGET = $(DATA)/crash.html
|
|
||||||
$(INLINE_TARGET): $(CSS_FULLTARGET) $(INLINE_SRC)
|
|
||||||
cp html/crash.html $(DATA)/crash.html
|
|
||||||
$(UNCSS) # generates $(DATA)/bundle.css for us
|
|
||||||
node scripts/inline.js root $(DATA) $(DATA)/crash.html $(DATA)/crash.html
|
|
||||||
rm $(DATA)/bundle.css
|
|
||||||
|
|
||||||
LANG_SRC = $(shell find ./lang)
|
|
||||||
LANG_TARGET = $(LANG_SRC:lang/%=$(DATA)/lang/%)
|
|
||||||
STATIC_SRC = $(wildcard static/*)
|
|
||||||
STATIC_TARGET = $(STATIC_SRC:static/%=$(DATA)/web/%)
|
|
||||||
COPY_SRC = images/banner.svg jfa-go.service LICENSE $(LANG_SRC) $(STATIC_SRC)
|
|
||||||
COPY_TARGET = $(DATA)/jfa-go.service
|
|
||||||
# $(DATA)/LICENSE $(LANG_TARGET) $(STATIC_TARGET) $(DATA)/web/css/$(CSSVERSION)bundle.css
|
|
||||||
$(COPY_TARGET): $(INLINE_TARGET) $(STATIC_SRC)
|
|
||||||
$(info copying crash page)
|
$(info copying crash page)
|
||||||
cp $(DATA)/crash.html $(DATA)/html/
|
mv $(DATA)/crash.html $(DATA)/html/
|
||||||
$(info copying static data)
|
$(info copying static data)
|
||||||
mkdir -p $(DATA)/web
|
mkdir -p $(DATA)/web
|
||||||
cp images/banner.svg static/banner.svg
|
cp images/banner.svg static/banner.svg
|
||||||
@ -208,22 +176,7 @@ $(COPY_TARGET): $(INLINE_TARGET) $(STATIC_SRC)
|
|||||||
$(info copying language files)
|
$(info copying language files)
|
||||||
cp -r lang $(DATA)/
|
cp -r lang $(DATA)/
|
||||||
cp LICENSE $(DATA)/
|
cp LICENSE $(DATA)/
|
||||||
|
mv $(DATA)/web/css/bundle.css $(DATA)/web/css/$(CSSVERSION)bundle.css
|
||||||
precompile: $(CONFIG_DESCRIPTION) $(CONFIG_DEFAULT) $(EMAIL_TARGET) $(COPY_TARGET) $(SWAGGER_TARGET)
|
|
||||||
|
|
||||||
GO_SRC = $(shell find ./ -name "*.go")
|
|
||||||
GO_TARGET = build/jfa-go
|
|
||||||
$(GO_TARGET): $(CONFIG_DESCRIPTION) $(CONFIG_DEFAULT) $(EMAIL_TARGET) $(COPY_TARGET) $(SWAGGER_TARGET) $(GO_SRC) go.mod go.sum
|
|
||||||
$(info Downloading deps)
|
|
||||||
$(GOBINARY) mod download
|
|
||||||
$(info Building)
|
|
||||||
mkdir -p build
|
|
||||||
$(GOBINARY) build $(RACEDETECTOR) -ldflags="$(LDFLAGS)" $(TAGS) -o $(GO_TARGET)
|
|
||||||
|
|
||||||
compile: $(GO_TARGET)
|
|
||||||
|
|
||||||
compress:
|
|
||||||
upx --lzma build/jfa-go
|
|
||||||
|
|
||||||
# internal-files:
|
# internal-files:
|
||||||
# python3 scripts/embed.py internal
|
# python3 scripts/embed.py internal
|
||||||
@ -244,6 +197,6 @@ clean:
|
|||||||
-rm docs/docs.go docs/swagger.json docs/swagger.yaml
|
-rm docs/docs.go docs/swagger.json docs/swagger.yaml
|
||||||
go clean
|
go clean
|
||||||
|
|
||||||
npm:
|
quick: configuration typescript variants-html bundle-css inline-css copy compile
|
||||||
$(info installing npm dependencies)
|
|
||||||
npm install $(NPMOPTS)
|
all: configuration npm email typescript variants-html bundle-css inline-css swagger copy compile
|
||||||
|
@ -144,13 +144,13 @@ func (app *appContext) GetActivities(gc *gin.Context) {
|
|||||||
if act.Type == ActivityDeletion || act.Type == ActivityCreation {
|
if act.Type == ActivityDeletion || act.Type == ActivityCreation {
|
||||||
resp.Activities[i].Username = act.Value
|
resp.Activities[i].Username = act.Value
|
||||||
resp.Activities[i].Value = ""
|
resp.Activities[i].Value = ""
|
||||||
} else if user, err := app.jf.UserByID(act.UserID, false); err == nil {
|
} else if user, status, err := app.jf.UserByID(act.UserID, false); status == 200 && err == nil {
|
||||||
resp.Activities[i].Username = user.Name
|
resp.Activities[i].Username = user.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
if (act.SourceType == ActivityUser || act.SourceType == ActivityAdmin) && act.Source != "" {
|
if (act.SourceType == ActivityUser || act.SourceType == ActivityAdmin) && act.Source != "" {
|
||||||
user, err := app.jf.UserByID(act.Source, false)
|
user, status, err := app.jf.UserByID(act.Source, false)
|
||||||
if err == nil {
|
if status == 200 && err == nil {
|
||||||
resp.Activities[i].SourceUsername = user.Name
|
resp.Activities[i].SourceUsername = user.Name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
74
api-ombi.go
74
api-ombi.go
@ -1,35 +1,34 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/hrfee/jfa-go/common"
|
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
lm "github.com/hrfee/jfa-go/logmessages"
|
||||||
"github.com/hrfee/jfa-go/ombi"
|
"github.com/hrfee/jfa-go/ombi"
|
||||||
"github.com/hrfee/mediabrowser"
|
"github.com/hrfee/mediabrowser"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, error) {
|
func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, int, error) {
|
||||||
jfUser, err := app.jf.UserByID(jfID, false)
|
jfUser, code, err := app.jf.UserByID(jfID, false)
|
||||||
if err != nil {
|
if err != nil || code != 200 {
|
||||||
return nil, err
|
return nil, code, err
|
||||||
}
|
}
|
||||||
username := jfUser.Name
|
username := jfUser.Name
|
||||||
email := ""
|
email := ""
|
||||||
if e, ok := app.storage.GetEmailsKey(jfID); ok {
|
if e, ok := app.storage.GetEmailsKey(jfID); ok {
|
||||||
email = e.Addr
|
email = e.Addr
|
||||||
}
|
}
|
||||||
user, err := app.ombi.getUser(username, email)
|
return app.ombi.getUser(username, email)
|
||||||
return user, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ombi *OmbiWrapper) getUser(username string, email string) (map[string]interface{}, error) {
|
func (ombi *OmbiWrapper) getUser(username string, email string) (map[string]interface{}, int, error) {
|
||||||
ombiUsers, err := ombi.GetUsers()
|
ombiUsers, code, err := ombi.GetUsers()
|
||||||
if err != nil {
|
if err != nil || code != 200 {
|
||||||
return nil, err
|
return nil, code, err
|
||||||
}
|
}
|
||||||
for _, ombiUser := range ombiUsers {
|
for _, ombiUser := range ombiUsers {
|
||||||
ombiAddr := ""
|
ombiAddr := ""
|
||||||
@ -37,19 +36,18 @@ func (ombi *OmbiWrapper) getUser(username string, email string) (map[string]inte
|
|||||||
ombiAddr = a.(string)
|
ombiAddr = a.(string)
|
||||||
}
|
}
|
||||||
if ombiUser["userName"].(string) == username || (ombiAddr == email && email != "") {
|
if ombiUser["userName"].(string) == username || (ombiAddr == email && email != "") {
|
||||||
return ombiUser, err
|
return ombiUser, code, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Gets a generic "not found" type error
|
return nil, 400, errors.New(lm.NotFound)
|
||||||
return nil, common.GenericErr(404, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a user with the given name who has been imported from Jellyfin/Emby by Ombi
|
// Returns a user with the given name who has been imported from Jellyfin/Emby by Ombi
|
||||||
func (ombi *OmbiWrapper) getImportedUser(name string) (map[string]interface{}, error) {
|
func (ombi *OmbiWrapper) getImportedUser(name string) (map[string]interface{}, int, error) {
|
||||||
// Ombi User Types: 3/4 = Emby, 5 = Jellyfin
|
// Ombi User Types: 3/4 = Emby, 5 = Jellyfin
|
||||||
ombiUsers, err := ombi.GetUsers()
|
ombiUsers, code, err := ombi.GetUsers()
|
||||||
if err != nil {
|
if err != nil || code != 200 {
|
||||||
return nil, err
|
return nil, code, err
|
||||||
}
|
}
|
||||||
for _, ombiUser := range ombiUsers {
|
for _, ombiUser := range ombiUsers {
|
||||||
if ombiUser["userName"].(string) == name {
|
if ombiUser["userName"].(string) == name {
|
||||||
@ -62,11 +60,10 @@ func (ombi *OmbiWrapper) getImportedUser(name string) (map[string]interface{}, e
|
|||||||
} else if uType != 3 && uType != 4 { // Emby
|
} else if uType != 3 && uType != 4 { // Emby
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return ombiUser, err
|
return ombiUser, code, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Gets a generic "not found" type error
|
return nil, 400, fmt.Errorf("couldn't find user")
|
||||||
return nil, common.GenericErr(404, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Summary Get a list of Ombi users.
|
// @Summary Get a list of Ombi users.
|
||||||
@ -77,8 +74,8 @@ func (ombi *OmbiWrapper) getImportedUser(name string) (map[string]interface{}, e
|
|||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
// @tags Ombi
|
// @tags Ombi
|
||||||
func (app *appContext) OmbiUsers(gc *gin.Context) {
|
func (app *appContext) OmbiUsers(gc *gin.Context) {
|
||||||
users, err := app.ombi.GetUsers()
|
users, status, err := app.ombi.GetUsers()
|
||||||
if err != nil {
|
if err != nil || status != 200 {
|
||||||
app.err.Printf(lm.FailedGetUsers, lm.Ombi, err)
|
app.err.Printf(lm.FailedGetUsers, lm.Ombi, err)
|
||||||
respond(500, "Couldn't get users", gc)
|
respond(500, "Couldn't get users", gc)
|
||||||
return
|
return
|
||||||
@ -113,8 +110,8 @@ func (app *appContext) SetOmbiProfile(gc *gin.Context) {
|
|||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
template, err := app.ombi.TemplateByID(req.ID)
|
template, code, err := app.ombi.TemplateByID(req.ID)
|
||||||
if err != nil || len(template) == 0 {
|
if err != nil || code != 200 || len(template) == 0 {
|
||||||
app.err.Printf(lm.FailedGetUsers, lm.Ombi, err)
|
app.err.Printf(lm.FailedGetUsers, lm.Ombi, err)
|
||||||
respond(500, "Couldn't get user", gc)
|
respond(500, "Couldn't get user", gc)
|
||||||
return
|
return
|
||||||
@ -150,7 +147,7 @@ type OmbiWrapper struct {
|
|||||||
*ombi.Ombi
|
*ombi.Ombi
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ombi *OmbiWrapper) applyProfile(user map[string]interface{}, profile map[string]interface{}) (err error) {
|
func (ombi *OmbiWrapper) applyProfile(user map[string]interface{}, profile map[string]interface{}) (status int, err error) {
|
||||||
for k, v := range profile {
|
for k, v := range profile {
|
||||||
switch v.(type) {
|
switch v.(type) {
|
||||||
case map[string]interface{}, []interface{}:
|
case map[string]interface{}, []interface{}:
|
||||||
@ -161,21 +158,22 @@ func (ombi *OmbiWrapper) applyProfile(user map[string]interface{}, profile map[s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = ombi.ModifyUser(user)
|
status, err = ombi.ModifyUser(user)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ombi *OmbiWrapper) ImportUser(jellyfinID string, req newUserDTO, profile Profile) (err error, ok bool) {
|
func (ombi *OmbiWrapper) ImportUser(jellyfinID string, req newUserDTO, profile Profile) (err error, ok bool) {
|
||||||
errors, err := ombi.NewUser(req.Username, req.Password, req.Email, profile.Ombi)
|
errors, code, err := ombi.NewUser(req.Username, req.Password, req.Email, profile.Ombi)
|
||||||
var ombiUser map[string]interface{}
|
var ombiUser map[string]interface{}
|
||||||
if err != nil {
|
var status int
|
||||||
|
if err != nil || code != 200 {
|
||||||
// Check if on the off chance, Ombi's user importer has already added the account.
|
// Check if on the off chance, Ombi's user importer has already added the account.
|
||||||
ombiUser, err = ombi.getImportedUser(req.Username)
|
ombiUser, status, err = ombi.getImportedUser(req.Username)
|
||||||
if err == nil {
|
if status == 200 && err == nil {
|
||||||
// app.info.Println(lm.Ombi + " " + lm.UserExists)
|
// app.info.Println(lm.Ombi + " " + lm.UserExists)
|
||||||
profile.Ombi["password"] = req.Password
|
profile.Ombi["password"] = req.Password
|
||||||
err = ombi.applyProfile(ombiUser, profile.Ombi)
|
status, err = ombi.applyProfile(ombiUser, profile.Ombi)
|
||||||
if err != nil {
|
if status != 200 || err != nil {
|
||||||
err = fmt.Errorf(lm.FailedApplyProfile, lm.Ombi, req.Username, err)
|
err = fmt.Errorf(lm.FailedApplyProfile, lm.Ombi, req.Username, err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -191,8 +189,9 @@ func (ombi *OmbiWrapper) ImportUser(jellyfinID string, req newUserDTO, profile P
|
|||||||
|
|
||||||
func (ombi *OmbiWrapper) AddContactMethods(jellyfinID string, req newUserDTO, discord *DiscordUser, telegram *TelegramUser) (err error) {
|
func (ombi *OmbiWrapper) AddContactMethods(jellyfinID string, req newUserDTO, discord *DiscordUser, telegram *TelegramUser) (err error) {
|
||||||
var ombiUser map[string]interface{}
|
var ombiUser map[string]interface{}
|
||||||
ombiUser, err = ombi.getUser(req.Username, req.Email)
|
var status int
|
||||||
if err != nil {
|
ombiUser, status, err = ombi.getUser(req.Username, req.Email)
|
||||||
|
if status != 200 || err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if discordEnabled || telegramEnabled {
|
if discordEnabled || telegramEnabled {
|
||||||
@ -205,8 +204,9 @@ func (ombi *OmbiWrapper) AddContactMethods(jellyfinID string, req newUserDTO, di
|
|||||||
tUser = telegram.Username
|
tUser = telegram.Username
|
||||||
}
|
}
|
||||||
var resp string
|
var resp string
|
||||||
resp, err = ombi.SetNotificationPrefs(ombiUser, dID, tUser)
|
var status int
|
||||||
if err != nil {
|
resp, status, err = ombi.SetNotificationPrefs(ombiUser, dID, tUser)
|
||||||
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
if resp != "" {
|
if resp != "" {
|
||||||
err = fmt.Errorf("%v, %s", err, resp)
|
err = fmt.Errorf("%v, %s", err, resp)
|
||||||
}
|
}
|
||||||
|
@ -84,8 +84,8 @@ func (app *appContext) CreateProfile(gc *gin.Context) {
|
|||||||
var req newProfileDTO
|
var req newProfileDTO
|
||||||
gc.BindJSON(&req)
|
gc.BindJSON(&req)
|
||||||
app.jf.CacheExpiry = time.Now()
|
app.jf.CacheExpiry = time.Now()
|
||||||
user, err := app.jf.UserByID(req.ID, false)
|
user, status, err := app.jf.UserByID(req.ID, false)
|
||||||
if err != nil {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
||||||
respond(500, "Couldn't get user", gc)
|
respond(500, "Couldn't get user", gc)
|
||||||
return
|
return
|
||||||
@ -98,8 +98,8 @@ func (app *appContext) CreateProfile(gc *gin.Context) {
|
|||||||
app.debug.Printf(lm.CreateProfileFromUser, user.Name)
|
app.debug.Printf(lm.CreateProfileFromUser, user.Name)
|
||||||
if req.Homescreen {
|
if req.Homescreen {
|
||||||
profile.Configuration = user.Configuration
|
profile.Configuration = user.Configuration
|
||||||
profile.Displayprefs, err = app.jf.GetDisplayPreferences(req.ID)
|
profile.Displayprefs, status, err = app.jf.GetDisplayPreferences(req.ID)
|
||||||
if err != nil {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
app.err.Printf(lm.FailedGetJellyfinDisplayPrefs, req.ID, err)
|
app.err.Printf(lm.FailedGetJellyfinDisplayPrefs, req.ID, err)
|
||||||
respond(500, "Couldn't get displayprefs", gc)
|
respond(500, "Couldn't get displayprefs", gc)
|
||||||
return
|
return
|
||||||
|
@ -29,8 +29,8 @@ func (app *appContext) MyDetails(gc *gin.Context) {
|
|||||||
Id: gc.GetString("jfId"),
|
Id: gc.GetString("jfId"),
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := app.jf.UserByID(resp.Id, false)
|
user, status, err := app.jf.UserByID(resp.Id, false)
|
||||||
if err != nil {
|
if status != 200 || err != nil {
|
||||||
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
||||||
respond(500, "Failed to get user", gc)
|
respond(500, "Failed to get user", gc)
|
||||||
return
|
return
|
||||||
@ -259,9 +259,9 @@ func (app *appContext) ModifyMyEmail(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if emailEnabled && app.config.Section("email_confirmation").Key("enabled").MustBool(false) {
|
if emailEnabled && app.config.Section("email_confirmation").Key("enabled").MustBool(false) {
|
||||||
user, err := app.jf.UserByID(id, false)
|
user, status, err := app.jf.UserByID(id, false)
|
||||||
name := ""
|
name := ""
|
||||||
if err == nil {
|
if status == 200 && err == nil {
|
||||||
name = user.Name
|
name = user.Name
|
||||||
}
|
}
|
||||||
app.debug.Printf(lm.EmailConfirmationRequired, id)
|
app.debug.Printf(lm.EmailConfirmationRequired, id)
|
||||||
@ -688,20 +688,20 @@ func (app *appContext) ChangeMyPassword(gc *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
user, err := app.jf.UserByID(gc.GetString("jfId"), false)
|
user, status, err := app.jf.UserByID(gc.GetString("jfId"), false)
|
||||||
if err != nil {
|
if status != 200 || err != nil {
|
||||||
app.err.Printf(lm.FailedGetUser, gc.GetString("jfId"), lm.Jellyfin, err)
|
app.err.Printf(lm.FailedGetUser, gc.GetString("jfId"), lm.Jellyfin, err)
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Authenticate as user to confirm old password.
|
// Authenticate as user to confirm old password.
|
||||||
user, err = app.authJf.Authenticate(user.Name, req.Old)
|
user, status, err = app.authJf.Authenticate(user.Name, req.Old)
|
||||||
if err != nil {
|
if status != 200 || err != nil {
|
||||||
respondBool(401, false, gc)
|
respondBool(401, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = app.jf.SetPassword(gc.GetString("jfId"), req.Old, req.New)
|
status, err = app.jf.SetPassword(gc.GetString("jfId"), req.Old, req.New)
|
||||||
if err != nil {
|
if (status != 200 && status != 204) || err != nil {
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -716,14 +716,14 @@ func (app *appContext) ChangeMyPassword(gc *gin.Context) {
|
|||||||
|
|
||||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||||
func() {
|
func() {
|
||||||
ombiUser, err := app.getOmbiUser(gc.GetString("jfId"))
|
ombiUser, status, err := app.getOmbiUser(gc.GetString("jfId"))
|
||||||
if err != nil {
|
if status != 200 || err != nil {
|
||||||
app.err.Printf(lm.FailedGetUser, user.Name, lm.Ombi, err)
|
app.err.Printf(lm.FailedGetUser, user.Name, lm.Ombi, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ombiUser["password"] = req.New
|
ombiUser["password"] = req.New
|
||||||
err = app.ombi.ModifyUser(ombiUser)
|
status, err = app.ombi.ModifyUser(ombiUser)
|
||||||
if err != nil {
|
if status != 200 || err != nil {
|
||||||
app.err.Printf(lm.FailedChangePassword, lm.Ombi, ombiUser["userName"], err)
|
app.err.Printf(lm.FailedChangePassword, lm.Ombi, ombiUser["userName"], err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
83
api-users.go
83
api-users.go
@ -379,9 +379,9 @@ func (app *appContext) EnableDisableUsers(gc *gin.Context) {
|
|||||||
activityType = ActivityEnabled
|
activityType = ActivityEnabled
|
||||||
}
|
}
|
||||||
for _, userID := range req.Users {
|
for _, userID := range req.Users {
|
||||||
user, err := app.jf.UserByID(userID, false)
|
user, status, err := app.jf.UserByID(userID, false)
|
||||||
if err != nil {
|
if status != 200 || err != nil {
|
||||||
errors["GetUser"][user.ID] = err.Error()
|
errors["GetUser"][user.ID] = fmt.Sprintf("%d %v", status, err)
|
||||||
app.err.Printf(lm.FailedGetUser, user.ID, lm.Jellyfin, err)
|
app.err.Printf(lm.FailedGetUser, user.ID, lm.Jellyfin, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -440,7 +440,10 @@ func (app *appContext) DeleteUsers(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, userID := range req.Users {
|
for _, userID := range req.Users {
|
||||||
user, err := app.jf.UserByID(userID, false)
|
user, status, err := app.jf.UserByID(userID, false)
|
||||||
|
if status != 200 && err == nil {
|
||||||
|
err = fmt.Errorf("failed (code %d)", status)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedGetUser, user.ID, lm.Jellyfin, err)
|
app.err.Printf(lm.FailedGetUser, user.ID, lm.Jellyfin, err)
|
||||||
errors[userID] = err.Error()
|
errors[userID] = err.Error()
|
||||||
@ -499,6 +502,7 @@ func (app *appContext) DeleteUsers(gc *gin.Context) {
|
|||||||
func (app *appContext) ExtendExpiry(gc *gin.Context) {
|
func (app *appContext) ExtendExpiry(gc *gin.Context) {
|
||||||
var req extendExpiryDTO
|
var req extendExpiryDTO
|
||||||
gc.BindJSON(&req)
|
gc.BindJSON(&req)
|
||||||
|
app.info.Printf("Expiry extension requested for %d user(s)", len(req.Users))
|
||||||
if req.Months <= 0 && req.Days <= 0 && req.Hours <= 0 && req.Minutes <= 0 && req.Timestamp <= 0 {
|
if req.Months <= 0 && req.Days <= 0 && req.Hours <= 0 && req.Minutes <= 0 && req.Timestamp <= 0 {
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
@ -518,8 +522,8 @@ func (app *appContext) ExtendExpiry(gc *gin.Context) {
|
|||||||
app.storage.SetUserExpiryKey(id, expiry)
|
app.storage.SetUserExpiryKey(id, expiry)
|
||||||
if messagesEnabled && req.Notify {
|
if messagesEnabled && req.Notify {
|
||||||
go func(uid string, exp time.Time) {
|
go func(uid string, exp time.Time) {
|
||||||
user, err := app.jf.UserByID(uid, false)
|
user, status, err := app.jf.UserByID(uid, false)
|
||||||
if err != nil {
|
if status != 200 || err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
msg, err := app.email.constructExpiryAdjusted(user.Name, exp, req.Reason, app, false)
|
msg, err := app.email.constructExpiryAdjusted(user.Name, exp, req.Reason, app, false)
|
||||||
@ -651,8 +655,8 @@ func (app *appContext) Announce(gc *gin.Context) {
|
|||||||
unique := strings.Contains(req.Message, "{username}")
|
unique := strings.Contains(req.Message, "{username}")
|
||||||
if unique {
|
if unique {
|
||||||
for _, userID := range req.Users {
|
for _, userID := range req.Users {
|
||||||
user, err := app.jf.UserByID(userID, false)
|
user, status, err := app.jf.UserByID(userID, false)
|
||||||
if err != nil {
|
if status != 200 || err != nil {
|
||||||
app.err.Printf(lm.FailedGetUser, userID, lm.Jellyfin, err)
|
app.err.Printf(lm.FailedGetUser, userID, lm.Jellyfin, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -831,9 +835,9 @@ func (app *appContext) AdminPasswordReset(gc *gin.Context) {
|
|||||||
// @tags Users
|
// @tags Users
|
||||||
func (app *appContext) GetUsers(gc *gin.Context) {
|
func (app *appContext) GetUsers(gc *gin.Context) {
|
||||||
var resp getUsersDTO
|
var resp getUsersDTO
|
||||||
users, err := app.jf.GetUsers(false)
|
users, status, err := app.jf.GetUsers(false)
|
||||||
resp.UserList = make([]respUser, len(users))
|
resp.UserList = make([]respUser, len(users))
|
||||||
if err != nil {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
||||||
respond(500, "Couldn't get users", gc)
|
respond(500, "Couldn't get users", gc)
|
||||||
return
|
return
|
||||||
@ -906,8 +910,8 @@ func (app *appContext) GetUsers(gc *gin.Context) {
|
|||||||
func (app *appContext) SetAccountsAdmin(gc *gin.Context) {
|
func (app *appContext) SetAccountsAdmin(gc *gin.Context) {
|
||||||
var req setAccountsAdminDTO
|
var req setAccountsAdminDTO
|
||||||
gc.BindJSON(&req)
|
gc.BindJSON(&req)
|
||||||
users, err := app.jf.GetUsers(false)
|
users, status, err := app.jf.GetUsers(false)
|
||||||
if err != nil {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
||||||
respond(500, "Couldn't get users", gc)
|
respond(500, "Couldn't get users", gc)
|
||||||
return
|
return
|
||||||
@ -938,8 +942,8 @@ func (app *appContext) SetAccountsAdmin(gc *gin.Context) {
|
|||||||
func (app *appContext) ModifyLabels(gc *gin.Context) {
|
func (app *appContext) ModifyLabels(gc *gin.Context) {
|
||||||
var req modifyEmailsDTO
|
var req modifyEmailsDTO
|
||||||
gc.BindJSON(&req)
|
gc.BindJSON(&req)
|
||||||
users, err := app.jf.GetUsers(false)
|
users, status, err := app.jf.GetUsers(false)
|
||||||
if err != nil {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
||||||
respond(500, "Couldn't get users", gc)
|
respond(500, "Couldn't get users", gc)
|
||||||
return
|
return
|
||||||
@ -972,11 +976,11 @@ func (app *appContext) modifyEmail(jfID string, addr string) {
|
|||||||
emailStore.Addr = addr
|
emailStore.Addr = addr
|
||||||
app.storage.SetEmailsKey(jfID, emailStore)
|
app.storage.SetEmailsKey(jfID, emailStore)
|
||||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||||
ombiUser, err := app.getOmbiUser(jfID)
|
ombiUser, code, err := app.getOmbiUser(jfID)
|
||||||
if err == nil {
|
if code == 200 && err == nil {
|
||||||
ombiUser["emailAddress"] = addr
|
ombiUser["emailAddress"] = addr
|
||||||
err = app.ombi.ModifyUser(ombiUser)
|
code, err = app.ombi.ModifyUser(ombiUser)
|
||||||
if err != nil {
|
if code != 200 || err != nil {
|
||||||
app.err.Printf(lm.FailedSetEmailAddress, lm.Ombi, jfID, err)
|
app.err.Printf(lm.FailedSetEmailAddress, lm.Ombi, jfID, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1008,8 +1012,8 @@ func (app *appContext) modifyEmail(jfID string, addr string) {
|
|||||||
func (app *appContext) ModifyEmails(gc *gin.Context) {
|
func (app *appContext) ModifyEmails(gc *gin.Context) {
|
||||||
var req modifyEmailsDTO
|
var req modifyEmailsDTO
|
||||||
gc.BindJSON(&req)
|
gc.BindJSON(&req)
|
||||||
users, err := app.jf.GetUsers(false)
|
users, status, err := app.jf.GetUsers(false)
|
||||||
if err != nil {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
||||||
respond(500, "Couldn't get users", gc)
|
respond(500, "Couldn't get users", gc)
|
||||||
return
|
return
|
||||||
@ -1095,8 +1099,8 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
|
|||||||
} else if req.From == "user" {
|
} else if req.From == "user" {
|
||||||
applyingFromType = lm.User
|
applyingFromType = lm.User
|
||||||
app.jf.CacheExpiry = time.Now()
|
app.jf.CacheExpiry = time.Now()
|
||||||
user, err := app.jf.UserByID(req.ID, false)
|
user, status, err := app.jf.UserByID(req.ID, false)
|
||||||
if err != nil {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
app.err.Printf(lm.FailedGetUser, req.ID, lm.Jellyfin, err)
|
app.err.Printf(lm.FailedGetUser, req.ID, lm.Jellyfin, err)
|
||||||
respond(500, "Couldn't get user", gc)
|
respond(500, "Couldn't get user", gc)
|
||||||
return
|
return
|
||||||
@ -1106,8 +1110,8 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
|
|||||||
policy = user.Policy
|
policy = user.Policy
|
||||||
}
|
}
|
||||||
if req.Homescreen {
|
if req.Homescreen {
|
||||||
displayprefs, err = app.jf.GetDisplayPreferences(req.ID)
|
displayprefs, status, err = app.jf.GetDisplayPreferences(req.ID)
|
||||||
if err != nil {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
app.err.Printf(lm.FailedGetJellyfinDisplayPrefs, req.ID, err)
|
app.err.Printf(lm.FailedGetJellyfinDisplayPrefs, req.ID, err)
|
||||||
respond(500, "Couldn't get displayprefs", gc)
|
respond(500, "Couldn't get displayprefs", gc)
|
||||||
return
|
return
|
||||||
@ -1132,25 +1136,26 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
|
|||||||
app.debug.Printf(lm.DelayingRequests, requestDelayThreshold)
|
app.debug.Printf(lm.DelayingRequests, requestDelayThreshold)
|
||||||
}
|
}
|
||||||
for _, id := range req.ApplyTo {
|
for _, id := range req.ApplyTo {
|
||||||
|
var status int
|
||||||
var err error
|
var err error
|
||||||
if req.Policy {
|
if req.Policy {
|
||||||
err = app.jf.SetPolicy(id, policy)
|
status, err = app.jf.SetPolicy(id, policy)
|
||||||
if err != nil {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
errors["policy"][id] = err.Error()
|
errors["policy"][id] = fmt.Sprintf("%d: %s", status, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if shouldDelay {
|
if shouldDelay {
|
||||||
time.Sleep(250 * time.Millisecond)
|
time.Sleep(250 * time.Millisecond)
|
||||||
}
|
}
|
||||||
if req.Homescreen {
|
if req.Homescreen {
|
||||||
err = app.jf.SetConfiguration(id, configuration)
|
status, err = app.jf.SetConfiguration(id, configuration)
|
||||||
errorString := ""
|
errorString := ""
|
||||||
if err != nil {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
errorString += fmt.Sprintf("Configuration: %v", err)
|
errorString += fmt.Sprintf("Configuration %d: %v ", status, err)
|
||||||
} else {
|
} else {
|
||||||
err = app.jf.SetDisplayPreferences(id, displayprefs)
|
status, err = app.jf.SetDisplayPreferences(id, displayprefs)
|
||||||
if err != nil {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
errorString += fmt.Sprintf("Displayprefs;) %v ", err)
|
errorString += fmt.Sprintf("Displayprefs %d: %v ", status, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if errorString != "" {
|
if errorString != "" {
|
||||||
@ -1159,18 +1164,18 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
if ombi != nil {
|
if ombi != nil {
|
||||||
errorString := ""
|
errorString := ""
|
||||||
user, err := app.getOmbiUser(id)
|
user, status, err := app.getOmbiUser(id)
|
||||||
if err != nil {
|
if status != 200 || err != nil {
|
||||||
errorString += fmt.Sprintf("Ombi GetUser: %v ", err)
|
errorString += fmt.Sprintf("Ombi GetUser %d: %v ", status, err)
|
||||||
} else {
|
} else {
|
||||||
// newUser := ombi
|
// newUser := ombi
|
||||||
// newUser["id"] = user["id"]
|
// newUser["id"] = user["id"]
|
||||||
// newUser["userName"] = user["userName"]
|
// newUser["userName"] = user["userName"]
|
||||||
// newUser["alias"] = user["alias"]
|
// newUser["alias"] = user["alias"]
|
||||||
// newUser["emailAddress"] = user["emailAddress"]
|
// newUser["emailAddress"] = user["emailAddress"]
|
||||||
err = app.ombi.applyProfile(user, ombi)
|
status, err = app.ombi.applyProfile(user, ombi)
|
||||||
if err != nil {
|
if status != 200 || err != nil {
|
||||||
errorString += fmt.Sprintf("Apply: %v ", err)
|
errorString += fmt.Sprintf("Apply %d: %v ", status, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if errorString != "" {
|
if errorString != "" {
|
||||||
|
31
api.go
31
api.go
@ -148,18 +148,18 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
|||||||
userID = reset.ID
|
userID = reset.ID
|
||||||
username = reset.Username
|
username = reset.Username
|
||||||
|
|
||||||
err := app.jf.ResetPasswordAdmin(userID)
|
status, err := app.jf.ResetPasswordAdmin(userID)
|
||||||
if err != nil {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, userID, err)
|
app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, userID, err)
|
||||||
respondBool(500, false, gc)
|
respondBool(status, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
delete(app.internalPWRs, req.PIN)
|
delete(app.internalPWRs, req.PIN)
|
||||||
} else {
|
} else {
|
||||||
resp, err := app.jf.ResetPassword(req.PIN)
|
resp, status, err := app.jf.ResetPassword(req.PIN)
|
||||||
if err != nil || !resp.Success {
|
if status != 200 || err != nil || !resp.Success {
|
||||||
app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, userID, err)
|
app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, userID, err)
|
||||||
respondBool(500, false, gc)
|
respondBool(status, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if req.Password == "" || len(resp.UsersReset) == 0 {
|
if req.Password == "" || len(resp.UsersReset) == 0 {
|
||||||
@ -170,13 +170,14 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var user mediabrowser.User
|
var user mediabrowser.User
|
||||||
|
var status int
|
||||||
var err error
|
var err error
|
||||||
if isInternal {
|
if isInternal {
|
||||||
user, err = app.jf.UserByID(userID, false)
|
user, status, err = app.jf.UserByID(userID, false)
|
||||||
} else {
|
} else {
|
||||||
user, err = app.jf.UserByName(username, false)
|
user, status, err = app.jf.UserByName(username, false)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if status != 200 || err != nil {
|
||||||
app.err.Printf(lm.FailedGetUser, userID, lm.Jellyfin, err)
|
app.err.Printf(lm.FailedGetUser, userID, lm.Jellyfin, err)
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
return
|
return
|
||||||
@ -194,8 +195,8 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
|||||||
if isInternal {
|
if isInternal {
|
||||||
prevPassword = ""
|
prevPassword = ""
|
||||||
}
|
}
|
||||||
err = app.jf.SetPassword(user.ID, prevPassword, req.Password)
|
status, err = app.jf.SetPassword(user.ID, prevPassword, req.Password)
|
||||||
if err != nil {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, user.ID, err)
|
app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, user.ID, err)
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
return
|
return
|
||||||
@ -209,15 +210,15 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
|||||||
respondBool(200, true, gc)
|
respondBool(200, true, gc)
|
||||||
return
|
return
|
||||||
} */
|
} */
|
||||||
ombiUser, err := app.getOmbiUser(user.ID)
|
ombiUser, status, err := app.getOmbiUser(user.ID)
|
||||||
if err != nil {
|
if status != 200 || err != nil {
|
||||||
app.err.Printf(lm.FailedGetUser, user.ID, lm.Ombi, err)
|
app.err.Printf(lm.FailedGetUser, user.ID, lm.Ombi, err)
|
||||||
respondBool(200, true, gc)
|
respondBool(200, true, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ombiUser["password"] = req.Password
|
ombiUser["password"] = req.Password
|
||||||
err = app.ombi.ModifyUser(ombiUser)
|
status, err = app.ombi.ModifyUser(ombiUser)
|
||||||
if err != nil {
|
if status != 200 || err != nil {
|
||||||
app.err.Printf(lm.FailedChangePassword, lm.Ombi, user.ID, err)
|
app.err.Printf(lm.FailedChangePassword, lm.Ombi, user.ID, err)
|
||||||
respondBool(200, true, gc)
|
respondBool(200, true, gc)
|
||||||
return
|
return
|
||||||
|
11
auth.go
11
auth.go
@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@ -167,19 +166,19 @@ func (app *appContext) decodeValidateLoginHeader(gc *gin.Context, userpage bool)
|
|||||||
|
|
||||||
func (app *appContext) validateJellyfinCredentials(username, password string, gc *gin.Context, userpage bool) (user mediabrowser.User, ok bool) {
|
func (app *appContext) validateJellyfinCredentials(username, password string, gc *gin.Context, userpage bool) (user mediabrowser.User, ok bool) {
|
||||||
ok = false
|
ok = false
|
||||||
user, err := app.authJf.Authenticate(username, password)
|
user, status, err := app.authJf.Authenticate(username, password)
|
||||||
if err != nil {
|
if status != 200 || err != nil {
|
||||||
if errors.Is(err, mediabrowser.ErrUnauthorized{}) {
|
if status == 401 || status == 400 {
|
||||||
app.logIpInfo(gc, userpage, fmt.Sprintf(lm.FailedAuthRequest, lm.InvalidUserOrPass))
|
app.logIpInfo(gc, userpage, fmt.Sprintf(lm.FailedAuthRequest, lm.InvalidUserOrPass))
|
||||||
respond(401, "Unauthorized", gc)
|
respond(401, "Unauthorized", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if errors.Is(err, mediabrowser.ErrForbidden{}) {
|
if status == 403 {
|
||||||
app.logIpInfo(gc, userpage, fmt.Sprintf(lm.FailedAuthRequest, lm.UserDisabled))
|
app.logIpInfo(gc, userpage, fmt.Sprintf(lm.FailedAuthRequest, lm.UserDisabled))
|
||||||
respond(403, "yourAccountWasDisabled", gc)
|
respond(403, "yourAccountWasDisabled", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.authLog(fmt.Sprintf(lm.FailedAuthJellyfin, app.jf.Server, err))
|
app.authLog(fmt.Sprintf(lm.FailedAuthJellyfin, app.jf.Server, status, err))
|
||||||
respond(500, "Jellyfin error", gc)
|
respond(500, "Jellyfin error", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TimeoutHandler recovers from an http timeout or panic.
|
// TimeoutHandler recovers from an http timeout or panic.
|
||||||
@ -15,7 +12,7 @@ type TimeoutHandler func()
|
|||||||
func NewTimeoutHandler(name, addr string, noFail bool) TimeoutHandler {
|
func NewTimeoutHandler(name, addr string, noFail bool) TimeoutHandler {
|
||||||
return func() {
|
return func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
out := fmt.Sprintf(lm.FailedAuth, name, addr, 0, lm.TimedOut)
|
out := fmt.Sprintf("Failed to authenticate with %s @ \"%s\": Timed out", name, addr)
|
||||||
if noFail {
|
if noFail {
|
||||||
log.Print(out)
|
log.Print(out)
|
||||||
} else {
|
} else {
|
||||||
@ -24,50 +21,3 @@ func NewTimeoutHandler(name, addr string, noFail bool) TimeoutHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// most 404 errors are from UserNotFound, so this generic error doesn't really need any detail.
|
|
||||||
type ErrNotFound error
|
|
||||||
|
|
||||||
type ErrUnauthorized struct{}
|
|
||||||
|
|
||||||
func (err ErrUnauthorized) Error() string {
|
|
||||||
return lm.Unauthorized
|
|
||||||
}
|
|
||||||
|
|
||||||
type ErrForbidden struct{}
|
|
||||||
|
|
||||||
func (err ErrForbidden) Error() string {
|
|
||||||
return lm.Forbidden
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
NotFound ErrNotFound = errors.New(lm.NotFound)
|
|
||||||
)
|
|
||||||
|
|
||||||
type ErrUnknown struct {
|
|
||||||
code int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err ErrUnknown) Error() string {
|
|
||||||
msg := fmt.Sprintf(lm.FailedGenericWithCode, err.code)
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenericErr returns an error appropriate to the given HTTP status (or actual error, if given).
|
|
||||||
func GenericErr(status int, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
switch status {
|
|
||||||
case 200, 204, 201:
|
|
||||||
return nil
|
|
||||||
case 401, 400:
|
|
||||||
return ErrUnauthorized{}
|
|
||||||
case 404:
|
|
||||||
return NotFound
|
|
||||||
case 403:
|
|
||||||
return ErrForbidden{}
|
|
||||||
default:
|
|
||||||
return ErrUnknown{code: status}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,7 +1,3 @@
|
|||||||
module github.com/hrfee/jfa-go/common
|
module github.com/hrfee/jfa-go/common
|
||||||
|
|
||||||
replace github.com/hrfee/jfa-go/logmessages => ../logmessages
|
go 1.15
|
||||||
|
|
||||||
go 1.22.4
|
|
||||||
|
|
||||||
require github.com/hrfee/jfa-go/logmessages v0.0.0-20240805130902-86c37fb4237b
|
|
||||||
|
13
css/base.css
13
css/base.css
@ -62,7 +62,18 @@ html:not(.dark) .card.\@low:not(.\~neutral):not(.\~positive):not(.\~urge):not(.\
|
|||||||
display: initial;
|
display: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 1024px) {
|
.page-container {
|
||||||
|
margin: 5% 20% 5% 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.page-container {
|
||||||
|
margin: 2%;
|
||||||
|
margin-top: 5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1000px) {
|
||||||
:root {
|
:root {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
21
email.go
21
email.go
@ -968,10 +968,11 @@ func (app *appContext) getAddressOrName(jfID string) string {
|
|||||||
// returns "" if none found. returns only the first match, might be an issue if there are users with the same contact method usernames.
|
// returns "" if none found. returns only the first match, might be an issue if there are users with the same contact method usernames.
|
||||||
func (app *appContext) ReverseUserSearch(address string, matchUsername, matchEmail, matchContactMethod bool) (user mediabrowser.User, ok bool) {
|
func (app *appContext) ReverseUserSearch(address string, matchUsername, matchEmail, matchContactMethod bool) (user mediabrowser.User, ok bool) {
|
||||||
ok = false
|
ok = false
|
||||||
|
var status int
|
||||||
var err error = nil
|
var err error = nil
|
||||||
if matchUsername {
|
if matchUsername {
|
||||||
user, err = app.jf.UserByName(address, false)
|
user, status, err = app.jf.UserByName(address, false)
|
||||||
if err == nil {
|
if status == 200 && err == nil {
|
||||||
ok = true
|
ok = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -982,8 +983,8 @@ func (app *appContext) ReverseUserSearch(address string, matchUsername, matchEma
|
|||||||
err = app.storage.db.Find(&emailAddresses, badgerhold.Where("Addr").Eq(address))
|
err = app.storage.db.Find(&emailAddresses, badgerhold.Where("Addr").Eq(address))
|
||||||
if err == nil && len(emailAddresses) > 0 {
|
if err == nil && len(emailAddresses) > 0 {
|
||||||
for _, emailUser := range emailAddresses {
|
for _, emailUser := range emailAddresses {
|
||||||
user, err = app.jf.UserByID(emailUser.JellyfinID, false)
|
user, status, err = app.jf.UserByID(emailUser.JellyfinID, false)
|
||||||
if err == nil {
|
if status == 200 && err == nil {
|
||||||
ok = true
|
ok = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -996,8 +997,8 @@ func (app *appContext) ReverseUserSearch(address string, matchUsername, matchEma
|
|||||||
if matchContactMethod {
|
if matchContactMethod {
|
||||||
for _, dcUser := range app.storage.GetDiscord() {
|
for _, dcUser := range app.storage.GetDiscord() {
|
||||||
if RenderDiscordUsername(dcUser) == strings.ToLower(address) {
|
if RenderDiscordUsername(dcUser) == strings.ToLower(address) {
|
||||||
user, err = app.jf.UserByID(dcUser.JellyfinID, false)
|
user, status, err = app.jf.UserByID(dcUser.JellyfinID, false)
|
||||||
if err == nil {
|
if status == 200 && err == nil {
|
||||||
ok = true
|
ok = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1008,8 +1009,8 @@ func (app *appContext) ReverseUserSearch(address string, matchUsername, matchEma
|
|||||||
err = app.storage.db.Find(&telegramUsers, badgerhold.Where("Username").Eq(tgUsername))
|
err = app.storage.db.Find(&telegramUsers, badgerhold.Where("Username").Eq(tgUsername))
|
||||||
if err == nil && len(telegramUsers) > 0 {
|
if err == nil && len(telegramUsers) > 0 {
|
||||||
for _, telegramUser := range telegramUsers {
|
for _, telegramUser := range telegramUsers {
|
||||||
user, err = app.jf.UserByID(telegramUser.JellyfinID, false)
|
user, status, err = app.jf.UserByID(telegramUser.JellyfinID, false)
|
||||||
if err == nil {
|
if status == 200 && err == nil {
|
||||||
ok = true
|
ok = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1019,8 +1020,8 @@ func (app *appContext) ReverseUserSearch(address string, matchUsername, matchEma
|
|||||||
err = app.storage.db.Find(&matrixUsers, badgerhold.Where("UserID").Eq(address))
|
err = app.storage.db.Find(&matrixUsers, badgerhold.Where("UserID").Eq(address))
|
||||||
if err == nil && len(matrixUsers) > 0 {
|
if err == nil && len(matrixUsers) > 0 {
|
||||||
for _, matrixUser := range matrixUsers {
|
for _, matrixUser := range matrixUsers {
|
||||||
user, err = app.jf.UserByID(matrixUser.JellyfinID, false)
|
user, status, err = app.jf.UserByID(matrixUser.JellyfinID, false)
|
||||||
if err == nil {
|
if status == 200 && err == nil {
|
||||||
ok = true
|
ok = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
10
go.mod
10
go.mod
@ -33,15 +33,13 @@ require (
|
|||||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
|
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||||
github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a
|
github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a
|
||||||
github.com/hrfee/jfa-go/common v0.0.0-20240805130902-86c37fb4237b
|
github.com/hrfee/jfa-go/common v0.0.0-20240728190513-dabef831d769
|
||||||
github.com/hrfee/jfa-go/docs v0.0.0-20230626224816-f72960635dc3
|
github.com/hrfee/jfa-go/docs v0.0.0-20230626224816-f72960635dc3
|
||||||
github.com/hrfee/jfa-go/easyproxy v0.0.0-00010101000000-000000000000
|
github.com/hrfee/jfa-go/easyproxy v0.0.0-00010101000000-000000000000
|
||||||
github.com/hrfee/jfa-go/jellyseerr v0.0.0-20240805130902-86c37fb4237b
|
|
||||||
github.com/hrfee/jfa-go/linecache v0.0.0-20230626224816-f72960635dc3
|
github.com/hrfee/jfa-go/linecache v0.0.0-20230626224816-f72960635dc3
|
||||||
github.com/hrfee/jfa-go/logger v0.0.0-20240731152135-2d066ea7cd32
|
github.com/hrfee/jfa-go/logger v0.0.0-20240731152135-2d066ea7cd32
|
||||||
github.com/hrfee/jfa-go/logmessages v0.0.0-20240805130902-86c37fb4237b
|
github.com/hrfee/jfa-go/ombi v0.0.0-20230626224816-f72960635dc3
|
||||||
github.com/hrfee/jfa-go/ombi v0.0.0-20240805130902-86c37fb4237b
|
github.com/hrfee/mediabrowser v0.3.14
|
||||||
github.com/hrfee/mediabrowser v0.3.18
|
|
||||||
github.com/itchyny/timefmt-go v0.1.5
|
github.com/itchyny/timefmt-go v0.1.5
|
||||||
github.com/lithammer/shortuuid/v3 v3.0.7
|
github.com/lithammer/shortuuid/v3 v3.0.7
|
||||||
github.com/mailgun/mailgun-go/v4 v4.9.1
|
github.com/mailgun/mailgun-go/v4 v4.9.1
|
||||||
@ -94,6 +92,8 @@ require (
|
|||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
github.com/gorilla/mux v1.8.0 // indirect
|
github.com/gorilla/mux v1.8.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.0 // indirect
|
github.com/gorilla/websocket v1.5.0 // indirect
|
||||||
|
github.com/hrfee/jfa-go/jellyseerr v0.0.0-00010101000000-000000000000 // indirect
|
||||||
|
github.com/hrfee/jfa-go/logmessages v0.0.0-00010101000000-000000000000 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/compress v1.16.6 // indirect
|
github.com/klauspost/compress v1.16.6 // indirect
|
||||||
|
16
go.sum
16
go.sum
@ -1,7 +1,6 @@
|
|||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
|
||||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||||
@ -95,7 +94,6 @@ github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgN
|
|||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
github.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w=
|
github.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w=
|
||||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||||
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
|
||||||
github.com/gin-contrib/pprof v1.4.0 h1:XxiBSf5jWZ5i16lNOPbMTVdgHBdhfGRD5PZ1LWazzvg=
|
github.com/gin-contrib/pprof v1.4.0 h1:XxiBSf5jWZ5i16lNOPbMTVdgHBdhfGRD5PZ1LWazzvg=
|
||||||
github.com/gin-contrib/pprof v1.4.0/go.mod h1:RrehPJasUVBPK6yTUwOl8/NP6i0vbUgmxtis+Z5KE90=
|
github.com/gin-contrib/pprof v1.4.0/go.mod h1:RrehPJasUVBPK6yTUwOl8/NP6i0vbUgmxtis+Z5KE90=
|
||||||
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
||||||
@ -142,7 +140,6 @@ github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogB
|
|||||||
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
|
||||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
@ -216,7 +213,6 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
@ -228,10 +224,10 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
|
|||||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
github.com/hrfee/mediabrowser v0.3.17 h1:gerX/sxQ8V64mZ13I0zuK9hpCJnScdTpAzD2j0a7bvE=
|
github.com/hrfee/mediabrowser v0.3.13 h1:NgQNbq+JWwsP68BdWXL/rwbpfE/oO5LJ5KVkE+aNbX8=
|
||||||
github.com/hrfee/mediabrowser v0.3.17/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
|
github.com/hrfee/mediabrowser v0.3.13/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
|
||||||
github.com/hrfee/mediabrowser v0.3.18 h1:6UDyae0srEVjiMG+utPQfJJp4UId6/T3WN9EDCQKRTk=
|
github.com/hrfee/mediabrowser v0.3.14 h1:/hxtwefV4pA63fyHussk00JBFry6p+uDDOumA49M1eY=
|
||||||
github.com/hrfee/mediabrowser v0.3.18/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
|
github.com/hrfee/mediabrowser v0.3.14/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
|
github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
|
||||||
github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
|
github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
|
||||||
@ -293,7 +289,6 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
|||||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
@ -356,7 +351,6 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
|||||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
|
||||||
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E=
|
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E=
|
||||||
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||||
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||||
@ -426,7 +420,6 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
|||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||||
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
||||||
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
|
||||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
@ -462,7 +455,6 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
|
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
|
||||||
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
@ -16,7 +16,7 @@ func (app *appContext) clearEmails() {
|
|||||||
app.debug.Println(lm.HousekeepingEmail)
|
app.debug.Println(lm.HousekeepingEmail)
|
||||||
emails := app.storage.GetEmails()
|
emails := app.storage.GetEmails()
|
||||||
for _, email := range emails {
|
for _, email := range emails {
|
||||||
_, err := app.jf.UserByID(email.JellyfinID, false)
|
_, _, err := app.jf.UserByID(email.JellyfinID, false)
|
||||||
// Make sure the user doesn't exist, and no other error has occured
|
// Make sure the user doesn't exist, and no other error has occured
|
||||||
switch err.(type) {
|
switch err.(type) {
|
||||||
case mediabrowser.ErrUserNotFound:
|
case mediabrowser.ErrUserNotFound:
|
||||||
@ -32,7 +32,7 @@ func (app *appContext) clearDiscord() {
|
|||||||
app.debug.Println(lm.HousekeepingDiscord)
|
app.debug.Println(lm.HousekeepingDiscord)
|
||||||
discordUsers := app.storage.GetDiscord()
|
discordUsers := app.storage.GetDiscord()
|
||||||
for _, discordUser := range discordUsers {
|
for _, discordUser := range discordUsers {
|
||||||
user, err := app.jf.UserByID(discordUser.JellyfinID, false)
|
user, _, err := app.jf.UserByID(discordUser.JellyfinID, false)
|
||||||
// Make sure the user doesn't exist, and no other error has occured
|
// Make sure the user doesn't exist, and no other error has occured
|
||||||
switch err.(type) {
|
switch err.(type) {
|
||||||
case mediabrowser.ErrUserNotFound:
|
case mediabrowser.ErrUserNotFound:
|
||||||
@ -53,7 +53,7 @@ func (app *appContext) clearMatrix() {
|
|||||||
app.debug.Println(lm.HousekeepingMatrix)
|
app.debug.Println(lm.HousekeepingMatrix)
|
||||||
matrixUsers := app.storage.GetMatrix()
|
matrixUsers := app.storage.GetMatrix()
|
||||||
for _, matrixUser := range matrixUsers {
|
for _, matrixUser := range matrixUsers {
|
||||||
_, err := app.jf.UserByID(matrixUser.JellyfinID, false)
|
_, _, err := app.jf.UserByID(matrixUser.JellyfinID, false)
|
||||||
// Make sure the user doesn't exist, and no other error has occured
|
// Make sure the user doesn't exist, and no other error has occured
|
||||||
switch err.(type) {
|
switch err.(type) {
|
||||||
case mediabrowser.ErrUserNotFound:
|
case mediabrowser.ErrUserNotFound:
|
||||||
@ -69,7 +69,7 @@ func (app *appContext) clearTelegram() {
|
|||||||
app.debug.Println(lm.HousekeepingTelegram)
|
app.debug.Println(lm.HousekeepingTelegram)
|
||||||
telegramUsers := app.storage.GetTelegram()
|
telegramUsers := app.storage.GetTelegram()
|
||||||
for _, telegramUser := range telegramUsers {
|
for _, telegramUser := range telegramUsers {
|
||||||
_, err := app.jf.UserByID(telegramUser.JellyfinID, false)
|
_, _, err := app.jf.UserByID(telegramUser.JellyfinID, false)
|
||||||
// Make sure the user doesn't exist, and no other error has occured
|
// Make sure the user doesn't exist, and no other error has occured
|
||||||
switch err.(type) {
|
switch err.(type) {
|
||||||
case mediabrowser.ErrUserNotFound:
|
case mediabrowser.ErrUserNotFound:
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<title>404 - jfa-go</title>
|
<title>404 - jfa-go</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="section">
|
<body class="section">
|
||||||
<div class="page-container m-2 lg:my-20 lg:mx-64">
|
<div class="page-container">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h1 class="heading">Page not found.</h1>
|
<h1 class="heading">Page not found.</h1>
|
||||||
<p class="content">
|
<p class="content">
|
||||||
|
118
html/admin.html
118
html/admin.html
@ -44,7 +44,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="modal-about" class="modal">
|
<div id="modal-about" class="modal">
|
||||||
<div class="relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/2 content card">
|
<div class="relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 content card">
|
||||||
<img src="{{ .urlBase }}/banner.svg" class="banner header" alt="jfa-go banner">
|
<img src="{{ .urlBase }}/banner.svg" class="banner header" alt="jfa-go banner">
|
||||||
<span class="heading"><span class="modal-close">×</span></span>
|
<span class="heading"><span class="modal-close">×</span></span>
|
||||||
<p>{{ .strings.version }} <span class="text-black dark:text-white font-mono bg-inherit">{{ .version }}</span></p>
|
<p>{{ .strings.version }} <span class="text-black dark:text-white font-mono bg-inherit">{{ .version }}</span></p>
|
||||||
@ -535,56 +535,58 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="notification-box"></div>
|
<div id="notification-box"></div>
|
||||||
<div class="page-container m-2 lg:my-20 lg:mx-64 flex flex-col gap-4">
|
<div class="top-4 left-4 absolute flex flex-row gap-2">
|
||||||
<div class="top-2 inset-x-2 lg:absolute flex flex-row justify-between">
|
<span class="dropdown z-[11]" tabindex="0" id="lang-dropdown">
|
||||||
<div class="flex flex-row gap-2">
|
<span class="button ~urge dropdown-button">
|
||||||
<span class="dropdown z-[11]" tabindex="0" id="lang-dropdown">
|
<i class="ri-global-line"></i>
|
||||||
<span class="button ~urge dropdown-button">
|
<span class="ml-2 chev"></span>
|
||||||
<i class="ri-global-line"></i>
|
</span>
|
||||||
<span class="ml-2 chev"></span>
|
<div class="dropdown-display">
|
||||||
</span>
|
<div class="card ~neutral @low">
|
||||||
<div class="dropdown-display">
|
<label class="switch pb-4">
|
||||||
<div class="card ~neutral @low">
|
<input type="radio" name="lang-time" id="lang-12h">
|
||||||
<label class="switch pb-4">
|
<span>{{ .strings.time12h }}</span>
|
||||||
<input type="radio" name="lang-time" id="lang-12h">
|
</label>
|
||||||
<span>{{ .strings.time12h }}</span>
|
<label class="switch pb-4">
|
||||||
</label>
|
<input type="radio" name="lang-time" id="lang-24h">
|
||||||
<label class="switch pb-4">
|
<span>{{ .strings.time24h }}</span>
|
||||||
<input type="radio" name="lang-time" id="lang-24h">
|
</label>
|
||||||
<span>{{ .strings.time24h }}</span>
|
<div id="lang-list"></div>
|
||||||
</label>
|
</div>
|
||||||
<div id="lang-list"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
<span class="button ~warning" alt="{{ .strings.theme }}" id="button-theme"><i class="ri-sun-line"></i></span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row gap-2">
|
</span>
|
||||||
<span class="button ~critical @low unfocused" id="logout-button">{{ .strings.logout }}</span>
|
<span class="button ~warning" alt="{{ .strings.theme }}" id="button-theme"><i class="ri-sun-line"></i></span>
|
||||||
{{ if .userPageEnabled }}
|
</div>
|
||||||
<div class="">
|
{{ if .userPageEnabled }}
|
||||||
<a class="button ~info" href="{{ .urlBase }}/my/account"><i class="ri-account-circle-fill mr-2"></i>{{ .strings.myAccount }}</a>
|
<div class="top-4 right-4 absolute">
|
||||||
</div>
|
<a class="button ~info" href="{{ .urlBase }}/my/account"><i class="ri-account-circle-fill mr-2"></i>{{ .strings.myAccount }}</a>
|
||||||
{{ end }}
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
<div class="page-container">
|
||||||
|
<div class="mb-4">
|
||||||
|
<header>
|
||||||
|
<div class="flex flex-row overflow-x-scroll items-center">
|
||||||
|
<span id="button-tab-invites" class="text-3xl button portal ~neutral dark:~d_neutral @low mr-2 mb-2 px-5">{{ .strings.invites }}</span>
|
||||||
|
<span id="button-tab-accounts" class="text-3xl button portal ~neutral dark:~d_neutral @low mr-2 mb-2 px-5">{{ .strings.accounts }}</span>
|
||||||
|
<span id="button-tab-activity" class="text-3xl button portal ~neutral dark:~d_neutral @low mr-2 mb-2 px-5">{{ .strings.activity }}</span>
|
||||||
|
<span id="button-tab-settings" class="text-3xl button portal ~neutral dark:~d_neutral @low mr-2 mb-2 px-5">{{ .strings.settings }}</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<div>
|
||||||
|
<span class="button ~critical @low mb-4 unfocused" id="logout-button">{{ .strings.logout }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<header>
|
<div id="tab-invites">
|
||||||
<div class="flex flex-row overflow-x-scroll items-center gap-2">
|
<div class="card @low invites dark:~d_neutral mb-4 overflow-visible">
|
||||||
<span id="button-tab-invites" class="text-3xl button portal ~neutral dark:~d_neutral @low px-5">{{ .strings.invites }}</span>
|
|
||||||
<span id="button-tab-accounts" class="text-3xl button portal ~neutral dark:~d_neutral @low px-5">{{ .strings.accounts }}</span>
|
|
||||||
<span id="button-tab-activity" class="text-3xl button portal ~neutral dark:~d_neutral @low px-5">{{ .strings.activity }}</span>
|
|
||||||
<span id="button-tab-settings" class="text-3xl button portal ~neutral dark:~d_neutral @low px-5">{{ .strings.settings }}</span>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div id="tab-invites" class="flex flex-col gap-4">
|
|
||||||
<div class="card @low dark:~d_neutral flex flex-col gap-2 overflow-visible invites">
|
|
||||||
<span class="heading">{{ .strings.invites }}</span>
|
<span class="heading">{{ .strings.invites }}</span>
|
||||||
<div id="invites"></div>
|
<div id="invites" class="mt-2"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card @low dark:~d_neutral flex flex-col gap-2">
|
<div class="card @low dark:~d_neutral">
|
||||||
<span class="heading">{{ .strings.create }}</span>
|
<span class="heading">{{ .strings.create }}</span>
|
||||||
<div class="flex flex-col md:flex-row gap-3" id="create-inv">
|
<div class="flex flex-col md:flex-row gap-3 mt-2" id="create-inv">
|
||||||
<div class="card ~neutral @low flex flex-col gap-2 flex-1">
|
<div class="card ~neutral @low flex flex-col gap-2 grow">
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<label class="w-1/2">
|
<label class="w-1/2">
|
||||||
<input type="radio" name="duration" class="unfocused" id="radio-inv-duration" checked>
|
<input type="radio" name="duration" class="unfocused" id="radio-inv-duration" checked>
|
||||||
@ -692,7 +694,7 @@
|
|||||||
<input type="text" id="create-user-label" class="input ~neutral @low">
|
<input type="text" id="create-user-label" class="input ~neutral @low">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card ~neutral @low flex flex-col justify-between gap-2 flex-1">
|
<div class="card ~neutral @low flex flex-col justify-between gap-2 grow">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<label class="label supra" for="create-uses">{{ .strings.inviteNumberOfUses }}</label>
|
<label class="label supra" for="create-uses">{{ .strings.inviteNumberOfUses }}</label>
|
||||||
@ -736,7 +738,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="tab-accounts" class="flex flex-col gap-4 unfocused">
|
<div id="tab-accounts" class="unfocused">
|
||||||
<div class="card @low dark:~d_neutral accounts mb-4 overflow-visible">
|
<div class="card @low dark:~d_neutral accounts mb-4 overflow-visible">
|
||||||
<div id="accounts-filter-dropdown" class="dropdown manual z-10 w-full">
|
<div id="accounts-filter-dropdown" class="dropdown manual z-10 w-full">
|
||||||
<div class="flex flex-col md:flex-row align-middle gap-2">
|
<div class="flex flex-col md:flex-row align-middle gap-2">
|
||||||
@ -835,7 +837,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="tab-activity" class="flex flex-col gap-4 unfocused">
|
<div id="tab-activity" class="unfocused">
|
||||||
<div class="card @low dark:~d_neutral activity mb-4 overflow-visible">
|
<div class="card @low dark:~d_neutral activity mb-4 overflow-visible">
|
||||||
<div id="activity-filter-dropdown" class="dropdown manual z-10 w-full" tabindex="0">
|
<div id="activity-filter-dropdown" class="dropdown manual z-10 w-full" tabindex="0">
|
||||||
<div class="flex flex-col md:flex-row align-middle gap-2">
|
<div class="flex flex-col md:flex-row align-middle gap-2">
|
||||||
@ -892,8 +894,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="tab-settings" class="flex flex-col gap-4 unfocused">
|
<div id="tab-settings" class="unfocused">
|
||||||
<div class="card @low dark:~d_neutral settings overflow flex flex-col gap-2">
|
<div class="card @low dark:~d_neutral settings overflow">
|
||||||
<div class="flex flex-col md:flex-row align-middle gap-2">
|
<div class="flex flex-col md:flex-row align-middle gap-2">
|
||||||
<div class="flex flex-row align-middle justify-between md:justify-normal">
|
<div class="flex flex-row align-middle justify-between md:justify-normal">
|
||||||
<span class="heading">{{ .strings.settings }}</span>
|
<span class="heading">{{ .strings.settings }}</span>
|
||||||
@ -910,18 +912,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col md:flex-row gap-3">
|
<div class="flex flex-col md:flex-row gap-3">
|
||||||
<div class="md:card @low dark:~d_neutral flex md:flex flex-col gap-2 flex-1" id="settings-sidebar">
|
<div class="card @low dark:~d_neutral col" id="settings-sidebar">
|
||||||
<div class="flex flex-row justify-between">
|
<div class="flex flex-row justify-between">
|
||||||
<input type="search" class="field ~neutral @low input settings-section-button justify-between" id="settings-search" placeholder="{{ .strings.search }}">
|
<input type="search" class="field ~neutral @low input settings-section-button justify-between mb-2" id="settings-search" placeholder="{{ .strings.search }}">
|
||||||
<button class="button ~neutral @low center -ml-10 rounded-s-none settings-search-clear" aria-label="{{ .strings.clearSearch }}" text="{{ .strings.clearSearch }}"><i class="ri-close-line"></i></button>
|
<button class="button ~neutral @low center -ml-10 rounded-s-none mb-2 settings-search-clear" aria-label="{{ .strings.clearSearch }}" text="{{ .strings.clearSearch }}"><i class="ri-close-line"></i></button>
|
||||||
</div>
|
|
||||||
<aside class="aside sm ~urge dark:~d_info @low" id="settings-message">Note: <span class="badge ~critical">*</span> indicates a required field, <span class="badge ~info dark:~d_warning">R</span> indicates changes require a restart.</aside>
|
|
||||||
<div id="settings-loader" class="flex flex-row gap-2">
|
|
||||||
<span class="button ~neutral @low settings-section-button justify-center grow" id="setting-about"><span class="flex">{{ .strings.aboutProgram }} <i class="ri-information-line ml-2"></i></span></span>
|
|
||||||
<span class="button ~neutral @low settings-section-button justify-center grow" id="setting-profiles"><span class="flex">{{ .strings.userProfiles }} <i class="ri-user-line ml-2"></i></span></span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<aside class="aside sm ~urge dark:~d_info mb-2 @low" id="settings-message">Note: <span class="badge ~critical">*</span> indicates a required field, <span class="badge ~info dark:~d_warning">R</span> indicates changes require a restart.</aside>
|
||||||
|
<span class="button ~neutral @low settings-section-button justify-between mb-2" id="setting-about"><span class="flex">{{ .strings.aboutProgram }} <i class="ri-information-line ml-2"></i></span></span>
|
||||||
|
<span class="button ~neutral @low settings-section-button justify-between mb-2" id="setting-profiles"><span class="flex">{{ .strings.userProfiles }} <i class="ri-user-line ml-2"></i></span></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card ~neutral @low overflow flex-1" id="settings-panel">
|
<div class="card ~neutral @low col overflow" id="settings-panel">
|
||||||
<div class="settings-section unfocused h-[100%]" id="settings-not-found">
|
<div class="settings-section unfocused h-[100%]" id="settings-not-found">
|
||||||
<div class="flex flex-col h-[100%] justify-center items-center">
|
<div class="flex flex-col h-[100%] justify-center items-center">
|
||||||
<span class="text-2xl font-medium italic mb-2">{{ .strings.noResultsFound }}</span>
|
<span class="text-2xl font-medium italic mb-2">{{ .strings.noResultsFound }}</span>
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<title>Crash report</title>
|
<title>Crash report</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="page-container m-2 lg:my-20 lg:mx-64">
|
<div class="page-container">
|
||||||
<div class="card ~critical sectioned">
|
<div class="card ~critical sectioned">
|
||||||
<section class="section ~critical">
|
<section class="section ~critical">
|
||||||
<span class="heading">Crash report for jfa-go</span>
|
<span class="heading">Crash report for jfa-go</span>
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<title>{{ .strings.successHeader }} - jfa-go</title>
|
<title>{{ .strings.successHeader }} - jfa-go</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="section">
|
<body class="section">
|
||||||
<div class="page-container m-2 lg:my-20 lg:mx-64">
|
<div class="page-container">
|
||||||
<div class="card ~neutral @low mb-4">
|
<div class="card ~neutral @low mb-4">
|
||||||
<span class="heading mb-4">{{ .strings.successHeader }}</span>
|
<span class="heading mb-4">{{ .strings.successHeader }}</span>
|
||||||
<p class="content my-4">{{ .successMessage }}</p>
|
<p class="content my-4">{{ .successMessage }}</p>
|
||||||
|
@ -35,7 +35,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ template "account-linking.html" . }}
|
{{ template "account-linking.html" . }}
|
||||||
<div class="top-2 left-2 absolute">
|
<div class="top-4 left-4 absolute">
|
||||||
<span class="dropdown" tabindex="0" id="lang-dropdown">
|
<span class="dropdown" tabindex="0" id="lang-dropdown">
|
||||||
<span class="button ~urge dropdown-button">
|
<span class="button ~urge dropdown-button">
|
||||||
<i class="ri-global-line"></i>
|
<i class="ri-global-line"></i>
|
||||||
@ -48,7 +48,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="notification-box"></div>
|
<div id="notification-box"></div>
|
||||||
<div class="page-container m-2 lg:my-20 lg:mx-64">
|
<div class="page-container">
|
||||||
<div class="card dark:~d_neutral @low">
|
<div class="card dark:~d_neutral @low">
|
||||||
<div class="flex flex-col md:flex-row gap-3 items-baseline mb-2">
|
<div class="flex flex-col md:flex-row gap-3 items-baseline mb-2">
|
||||||
<span class="heading mr-5">
|
<span class="heading mr-5">
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<title>Invalid Code - jfa-go</title>
|
<title>Invalid Code - jfa-go</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="section">
|
<body class="section">
|
||||||
<div class="page-container m-2 lg:my-20 lg:mx-64">
|
<div class="page-container">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h1 class="text-3xl font-semibold">Invalid invite code.</h1>
|
<h1 class="text-3xl font-semibold">Invalid invite code.</h1>
|
||||||
<p class="content">The code above was either incorrect, or has expired.</p>
|
<p class="content">The code above was either incorrect, or has expired.</p>
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
<span id="copy-notification" class="unfocused">{{ .strings.copied }}</span>
|
<span id="copy-notification" class="unfocused">{{ .strings.copied }}</span>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<div class="page-container m-2 lg:my-20 lg:mx-64">
|
<div class="page-container">
|
||||||
<div class="card ~neutral @low mb-4">
|
<div class="card ~neutral @low mb-4">
|
||||||
<span class="heading mb-4">
|
<span class="heading mb-4">
|
||||||
{{ if .success }}
|
{{ if .success }}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body class="max-w-full overflow-x-hidden section">
|
<body class="max-w-full overflow-x-hidden section">
|
||||||
<div id="notification-box"></div>
|
<div id="notification-box"></div>
|
||||||
<div class="top-2 left-2 absolute">
|
<div class="top-4 left-4 absolute">
|
||||||
<span class="dropdown" tabindex="0" id="lang-dropdown">
|
<span class="dropdown" tabindex="0" id="lang-dropdown">
|
||||||
<span class="button ~urge dropdown-button">
|
<span class="button ~urge dropdown-button">
|
||||||
<i class="ri-global-line"></i>
|
<i class="ri-global-line"></i>
|
||||||
@ -19,7 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="page-container m-2 lg:my-20 lg:mx-64" id="page-container">
|
<div class="page-container" id="page-container">
|
||||||
<div class="card ~neutral @low mb-2">
|
<div class="card ~neutral @low mb-2">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<img class="banner header" src="banner.svg" alt="jfa-go" />
|
<img class="banner header" src="banner.svg" alt="jfa-go" />
|
||||||
|
@ -79,7 +79,7 @@
|
|||||||
{{ template "login-modal.html" . }}
|
{{ template "login-modal.html" . }}
|
||||||
{{ template "account-linking.html" . }}
|
{{ template "account-linking.html" . }}
|
||||||
<div id="notification-box"></div>
|
<div id="notification-box"></div>
|
||||||
<div class="top-2 left-2 absolute">
|
<div class="top-4 left-4 absolute">
|
||||||
<span class="dropdown" tabindex="0" id="lang-dropdown">
|
<span class="dropdown" tabindex="0" id="lang-dropdown">
|
||||||
<span class="button ~urge dropdown-button">
|
<span class="button ~urge dropdown-button">
|
||||||
<i class="ri-global-line"></i>
|
<i class="ri-global-line"></i>
|
||||||
@ -102,10 +102,10 @@
|
|||||||
<span class="button ~warning" alt="{{ .strings.theme }}" id="button-theme"><i class="ri-sun-line"></i></span>
|
<span class="button ~warning" alt="{{ .strings.theme }}" id="button-theme"><i class="ri-sun-line"></i></span>
|
||||||
<span class="button ~critical @low mb-4 unfocused" id="logout-button">{{ .strings.logout }}</span>
|
<span class="button ~critical @low mb-4 unfocused" id="logout-button">{{ .strings.logout }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="top-2 right-2 absolute">
|
<div class="top-4 right-4 absolute">
|
||||||
<a class="button ~info unfocused" href="/" id="admin-back-button"><i class="ri-arrow-left-fill mr-2"></i>{{ .strings.admin }}</a>
|
<a class="button ~info unfocused" href="/" id="admin-back-button"><i class="ri-arrow-left-fill mr-2"></i>{{ .strings.admin }}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="page-container m-2 lg:my-20 lg:mx-64 unfocused">
|
<div class="page-container unfocused">
|
||||||
<div class="card @low dark:~d_neutral mb-4" id="card-user">
|
<div class="card @low dark:~d_neutral mb-4" id="card-user">
|
||||||
<span class="heading mb-2"></span>
|
<span class="heading mb-2"></span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -58,8 +58,8 @@ func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) SynchronizeJellyseerrUsers() {
|
func (app *appContext) SynchronizeJellyseerrUsers() {
|
||||||
users, err := app.jf.GetUsers(false)
|
users, status, err := app.jf.GetUsers(false)
|
||||||
if err != nil {
|
if err != nil || status != 200 {
|
||||||
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
co "github.com/hrfee/jfa-go/common"
|
"github.com/hrfee/jfa-go/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -28,13 +28,13 @@ type Jellyseerr struct {
|
|||||||
userCache map[string]User // Map of jellyfin IDs to users
|
userCache map[string]User // Map of jellyfin IDs to users
|
||||||
cacheExpiry time.Time
|
cacheExpiry time.Time
|
||||||
cacheLength time.Duration
|
cacheLength time.Duration
|
||||||
timeoutHandler co.TimeoutHandler
|
timeoutHandler common.TimeoutHandler
|
||||||
LogRequestBodies bool
|
LogRequestBodies bool
|
||||||
AutoImportUsers bool
|
AutoImportUsers bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewJellyseerr returns an Ombi object.
|
// NewJellyseerr returns an Ombi object.
|
||||||
func NewJellyseerr(server, key string, timeoutHandler co.TimeoutHandler) *Jellyseerr {
|
func NewJellyseerr(server, key string, timeoutHandler common.TimeoutHandler) *Jellyseerr {
|
||||||
if !strings.HasSuffix(server, API_SUFFIX) {
|
if !strings.HasSuffix(server, API_SUFFIX) {
|
||||||
server = server + API_SUFFIX
|
server = server + API_SUFFIX
|
||||||
}
|
}
|
||||||
@ -82,23 +82,25 @@ func (js *Jellyseerr) req(mode string, uri string, data any, queryParams url.Val
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
resp, err := js.httpClient.Do(req)
|
resp, err := js.httpClient.Do(req)
|
||||||
err = co.GenericErr(resp.StatusCode, err)
|
reqFailed := err != nil || !(resp.StatusCode == 200 || resp.StatusCode == 201)
|
||||||
defer js.timeoutHandler()
|
defer js.timeoutHandler()
|
||||||
var responseText string
|
var responseText string
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if response || err != nil {
|
if response || reqFailed {
|
||||||
responseText, err = js.decodeResp(resp)
|
responseText, err = js.decodeResp(resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return responseText, resp.StatusCode, err
|
return responseText, resp.StatusCode, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if reqFailed {
|
||||||
var msg ErrorDTO
|
var msg ErrorDTO
|
||||||
err = json.Unmarshal([]byte(responseText), &msg)
|
err = json.Unmarshal([]byte(responseText), &msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return responseText, resp.StatusCode, err
|
return responseText, resp.StatusCode, err
|
||||||
}
|
}
|
||||||
if msg.Message != "" {
|
if msg.Message == "" {
|
||||||
|
err = fmt.Errorf("failed (error %d)", resp.StatusCode)
|
||||||
|
} else {
|
||||||
err = fmt.Errorf("got %d: %s", resp.StatusCode, msg.Message)
|
err = fmt.Errorf("got %d: %s", resp.StatusCode, msg.Message)
|
||||||
}
|
}
|
||||||
return responseText, resp.StatusCode, err
|
return responseText, resp.StatusCode, err
|
||||||
@ -143,11 +145,14 @@ func (js *Jellyseerr) ImportFromJellyfin(jfIDs ...string) ([]User, error) {
|
|||||||
params := map[string]interface{}{
|
params := map[string]interface{}{
|
||||||
"jellyfinUserIds": jfIDs,
|
"jellyfinUserIds": jfIDs,
|
||||||
}
|
}
|
||||||
resp, _, err := js.post(js.server+"/user/import-from-jellyfin", params, true)
|
resp, status, err := js.post(js.server+"/user/import-from-jellyfin", params, true)
|
||||||
var data []User
|
var data []User
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
|
if status != 200 && status != 201 {
|
||||||
|
return data, fmt.Errorf("failed (error %d)", status)
|
||||||
|
}
|
||||||
err = json.Unmarshal([]byte(resp), &data)
|
err = json.Unmarshal([]byte(resp), &data)
|
||||||
for _, u := range data {
|
for _, u := range data {
|
||||||
if u.JellyfinUserID != "" {
|
if u.JellyfinUserID != "" {
|
||||||
@ -192,11 +197,15 @@ func (js *Jellyseerr) getUserPage(page int) (GetUsersDTO, error) {
|
|||||||
if js.LogRequestBodies {
|
if js.LogRequestBodies {
|
||||||
fmt.Printf("Jellyseerr API Client: Sending with URL params \"%+v\"\n", params)
|
fmt.Printf("Jellyseerr API Client: Sending with URL params \"%+v\"\n", params)
|
||||||
}
|
}
|
||||||
resp, _, err := js.get(js.server+"/user", nil, params)
|
resp, status, err := js.get(js.server+"/user", nil, params)
|
||||||
var data GetUsersDTO
|
var data GetUsersDTO
|
||||||
if err == nil {
|
if status != 200 {
|
||||||
err = json.Unmarshal([]byte(resp), &data)
|
return data, fmt.Errorf("failed (error %d)", status)
|
||||||
}
|
}
|
||||||
|
if err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
err = json.Unmarshal([]byte(resp), &data)
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,9 +261,12 @@ func (js *Jellyseerr) getUser(jfID string) (User, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (js *Jellyseerr) Me() (User, error) {
|
func (js *Jellyseerr) Me() (User, error) {
|
||||||
resp, _, err := js.get(js.server+"/auth/me", nil, url.Values{})
|
resp, status, err := js.get(js.server+"/auth/me", nil, url.Values{})
|
||||||
var data User
|
var data User
|
||||||
data.ID = -1
|
data.ID = -1
|
||||||
|
if status != 200 {
|
||||||
|
return data, fmt.Errorf("failed (error %d)", status)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
@ -269,10 +281,13 @@ func (js *Jellyseerr) GetPermissions(jfID string) (Permissions, error) {
|
|||||||
return data.Permissions, err
|
return data.Permissions, err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, _, err := js.get(fmt.Sprintf(js.server+"/user/%d/settings/permissions", u.ID), nil, url.Values{})
|
resp, status, err := js.get(fmt.Sprintf(js.server+"/user/%d/settings/permissions", u.ID), nil, url.Values{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return data.Permissions, err
|
return data.Permissions, err
|
||||||
}
|
}
|
||||||
|
if status != 200 {
|
||||||
|
return data.Permissions, fmt.Errorf("failed (error %d)", status)
|
||||||
|
}
|
||||||
err = json.Unmarshal([]byte(resp), &data)
|
err = json.Unmarshal([]byte(resp), &data)
|
||||||
return data.Permissions, err
|
return data.Permissions, err
|
||||||
}
|
}
|
||||||
@ -283,10 +298,13 @@ func (js *Jellyseerr) SetPermissions(jfID string, perm Permissions) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _, err = js.post(fmt.Sprintf(js.server+"/user/%d/settings/permissions", u.ID), permissionsDTO{Permissions: perm}, false)
|
_, status, err := js.post(fmt.Sprintf(js.server+"/user/%d/settings/permissions", u.ID), permissionsDTO{Permissions: perm}, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if status != 200 && status != 201 {
|
||||||
|
return fmt.Errorf("failed (error %d)", status)
|
||||||
|
}
|
||||||
u.Permissions = perm
|
u.Permissions = perm
|
||||||
js.userCache[jfID] = u
|
js.userCache[jfID] = u
|
||||||
return nil
|
return nil
|
||||||
@ -298,10 +316,13 @@ func (js *Jellyseerr) ApplyTemplateToUser(jfID string, tmpl UserTemplate) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _, err = js.put(fmt.Sprintf(js.server+"/user/%d", u.ID), tmpl, false)
|
_, status, err := js.put(fmt.Sprintf(js.server+"/user/%d", u.ID), tmpl, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if status != 200 && status != 201 {
|
||||||
|
return fmt.Errorf("failed (error %d)", status)
|
||||||
|
}
|
||||||
u.UserTemplate = tmpl
|
u.UserTemplate = tmpl
|
||||||
js.userCache[jfID] = u
|
js.userCache[jfID] = u
|
||||||
return nil
|
return nil
|
||||||
@ -316,10 +337,13 @@ func (js *Jellyseerr) ModifyUser(jfID string, conf map[UserField]any) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _, err = js.put(fmt.Sprintf(js.server+"/user/%d", u.ID), conf, false)
|
_, status, err := js.put(fmt.Sprintf(js.server+"/user/%d", u.ID), conf, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if status != 200 && status != 201 {
|
||||||
|
return fmt.Errorf("failed (error %d)", status)
|
||||||
|
}
|
||||||
// Lazily just invalidate the cache.
|
// Lazily just invalidate the cache.
|
||||||
js.cacheExpiry = time.Now()
|
js.cacheExpiry = time.Now()
|
||||||
return nil
|
return nil
|
||||||
@ -331,7 +355,10 @@ func (js *Jellyseerr) DeleteUser(jfID string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = js.delete(fmt.Sprintf(js.server+"/user/%d", u.ID), nil)
|
status, err := js.delete(fmt.Sprintf(js.server+"/user/%d", u.ID), nil)
|
||||||
|
if status != 200 && status != 201 {
|
||||||
|
return fmt.Errorf("failed (error %d)", status)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -349,10 +376,13 @@ func (js *Jellyseerr) GetNotificationPreferences(jfID string) (Notifications, er
|
|||||||
|
|
||||||
func (js *Jellyseerr) GetNotificationPreferencesByID(jellyseerrID int64) (Notifications, error) {
|
func (js *Jellyseerr) GetNotificationPreferencesByID(jellyseerrID int64) (Notifications, error) {
|
||||||
var data Notifications
|
var data Notifications
|
||||||
resp, _, err := js.get(fmt.Sprintf(js.server+"/user/%d/settings/notifications", jellyseerrID), nil, url.Values{})
|
resp, status, err := js.get(fmt.Sprintf(js.server+"/user/%d/settings/notifications", jellyseerrID), nil, url.Values{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
|
if status != 200 {
|
||||||
|
return data, fmt.Errorf("failed (error %d)", status)
|
||||||
|
}
|
||||||
err = json.Unmarshal([]byte(resp), &data)
|
err = json.Unmarshal([]byte(resp), &data)
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
@ -367,10 +397,13 @@ func (js *Jellyseerr) ApplyNotificationsTemplateToUser(jfID string, tmpl Notific
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _, err = js.post(fmt.Sprintf(js.server+"/user/%d/settings/notifications", u.ID), tmpl, false)
|
_, status, err := js.post(fmt.Sprintf(js.server+"/user/%d/settings/notifications", u.ID), tmpl, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if status != 200 && status != 201 {
|
||||||
|
return fmt.Errorf("failed (error %d)", status)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -380,10 +413,13 @@ func (js *Jellyseerr) ModifyNotifications(jfID string, conf map[NotificationsFie
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _, err = js.post(fmt.Sprintf(js.server+"/user/%d/settings/notifications", u.ID), conf, false)
|
_, status, err := js.post(fmt.Sprintf(js.server+"/user/%d/settings/notifications", u.ID), conf, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if status != 200 && status != 201 {
|
||||||
|
return fmt.Errorf("failed (error %d)", status)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -393,8 +429,11 @@ func (js *Jellyseerr) GetUsers() (map[string]User, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (js *Jellyseerr) UserByID(jellyseerrID int64) (User, error) {
|
func (js *Jellyseerr) UserByID(jellyseerrID int64) (User, error) {
|
||||||
resp, _, err := js.get(js.server+fmt.Sprintf("/user/%d", jellyseerrID), nil, url.Values{})
|
resp, status, err := js.get(js.server+fmt.Sprintf("/user/%d", jellyseerrID), nil, url.Values{})
|
||||||
var data User
|
var data User
|
||||||
|
if status != 200 {
|
||||||
|
return data, fmt.Errorf("failed (error %d)", status)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
@ -408,10 +447,13 @@ func (js *Jellyseerr) ModifyMainUserSettings(jfID string, conf MainUserSettings)
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _, err = js.post(fmt.Sprintf(js.server+"/user/%d/settings/main", u.ID), conf, false)
|
_, status, err := js.post(fmt.Sprintf(js.server+"/user/%d/settings/main", u.ID), conf, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if status != 200 && status != 201 {
|
||||||
|
return fmt.Errorf("failed (error %d)", status)
|
||||||
|
}
|
||||||
// Lazily just invalidate the cache.
|
// Lazily just invalidate the cache.
|
||||||
js.cacheExpiry = time.Now()
|
js.cacheExpiry = time.Now()
|
||||||
return nil
|
return nil
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
module github.com/hrfee/jfa-go/logmessages
|
module github.com/hrfee/logmessages
|
||||||
|
|
||||||
go 1.22.4
|
go 1.22.4
|
||||||
|
@ -45,7 +45,7 @@ const (
|
|||||||
|
|
||||||
UsingTLS = "Using TLS/HTTP2"
|
UsingTLS = "Using TLS/HTTP2"
|
||||||
|
|
||||||
UsingOmbi = "Starting " + Ombi + " client"
|
UsingOmbi = "Starting " + " + Ombi + " + " client"
|
||||||
UsingJellyseerr = "Starting " + Jellyseerr + " client"
|
UsingJellyseerr = "Starting " + Jellyseerr + " client"
|
||||||
UsingEmby = "Using Emby server type (EXPERIMENTAL: PWRs are not available, and support is limited.)"
|
UsingEmby = "Using Emby server type (EXPERIMENTAL: PWRs are not available, and support is limited.)"
|
||||||
UsingJellyfin = "Using " + Jellyfin + " server type"
|
UsingJellyfin = "Using " + Jellyfin + " server type"
|
||||||
@ -54,13 +54,6 @@ const (
|
|||||||
|
|
||||||
AuthJellyfin = "Authenticated with " + Jellyfin + " @ \"%s\""
|
AuthJellyfin = "Authenticated with " + Jellyfin + " @ \"%s\""
|
||||||
FailedAuthJellyfin = "Failed to authenticate with " + Jellyfin + " @ \"%s\" (code %d): %v"
|
FailedAuthJellyfin = "Failed to authenticate with " + Jellyfin + " @ \"%s\" (code %d): %v"
|
||||||
FailedAuth = "Failed to authenticate with %s @ \"%s\" (code %d): %v"
|
|
||||||
|
|
||||||
Unauthorized = "unauthorized, check credentials"
|
|
||||||
Forbidden = "forbidden, the user may not have correct permissions"
|
|
||||||
NotFound = "not found"
|
|
||||||
TimedOut = "timed out"
|
|
||||||
FailedGenericWithCode = "failed (code %d)"
|
|
||||||
|
|
||||||
InitDiscord = "Initialized Discord daemon"
|
InitDiscord = "Initialized Discord daemon"
|
||||||
FailedInitDiscord = "Failed to initialize Discord daemon: %v"
|
FailedInitDiscord = "Failed to initialize Discord daemon: %v"
|
||||||
@ -220,6 +213,7 @@ const (
|
|||||||
FailedGetDiscordChannel = "Failed to get " + Discord + " channel \"%s\": %v"
|
FailedGetDiscordChannel = "Failed to get " + Discord + " channel \"%s\": %v"
|
||||||
MonitorAllDiscordChannels = "Will monitor all " + Discord + " channels"
|
MonitorAllDiscordChannels = "Will monitor all " + Discord + " channels"
|
||||||
FailedCreateDiscordDMChannel = "Failed to create " + Discord + " private DM channel with \"%s\": %v"
|
FailedCreateDiscordDMChannel = "Failed to create " + Discord + " private DM channel with \"%s\": %v"
|
||||||
|
NotFound = "not found"
|
||||||
RegisterDiscordChoice = "Registered " + Discord + " %s choice \"%s\""
|
RegisterDiscordChoice = "Registered " + Discord + " %s choice \"%s\""
|
||||||
FailedRegisterDiscordChoices = "Failed to register " + Discord + " %s choices: %v"
|
FailedRegisterDiscordChoices = "Failed to register " + Discord + " %s choices: %v"
|
||||||
FailedDeregDiscordChoice = "Failed to deregister " + Discord + " %s choice \"%s\": %v"
|
FailedDeregDiscordChoice = "Failed to deregister " + Discord + " %s choice \"%s\": %v"
|
||||||
|
12
main.go
12
main.go
@ -154,8 +154,8 @@ func test(app *appContext) {
|
|||||||
for n, v := range settings {
|
for n, v := range settings {
|
||||||
fmt.Println(n, ":", v)
|
fmt.Println(n, ":", v)
|
||||||
}
|
}
|
||||||
users, err := app.jf.GetUsers(false)
|
users, status, err := app.jf.GetUsers(false)
|
||||||
fmt.Printf("GetUsers: err %s maplength %d\n", err, len(users))
|
fmt.Printf("GetUsers: code %d err %s maplength %d\n", status, err, len(users))
|
||||||
fmt.Printf("View output? [y/n]: ")
|
fmt.Printf("View output? [y/n]: ")
|
||||||
var choice string
|
var choice string
|
||||||
fmt.Scanln(&choice)
|
fmt.Scanln(&choice)
|
||||||
@ -166,8 +166,8 @@ func test(app *appContext) {
|
|||||||
fmt.Printf("Enter a user to grab: ")
|
fmt.Printf("Enter a user to grab: ")
|
||||||
var username string
|
var username string
|
||||||
fmt.Scanln(&username)
|
fmt.Scanln(&username)
|
||||||
user, err := app.jf.UserByName(username, false)
|
user, status, err := app.jf.UserByName(username, false)
|
||||||
fmt.Printf("UserByName (%s): code %d err %s", username, err)
|
fmt.Printf("UserByName (%s): code %d err %s", username, status, err)
|
||||||
out, _ := json.MarshalIndent(user, "", " ")
|
out, _ := json.MarshalIndent(user, "", " ")
|
||||||
fmt.Print(string(out))
|
fmt.Print(string(out))
|
||||||
}
|
}
|
||||||
@ -436,8 +436,8 @@ func start(asDaemon, firstCall bool) {
|
|||||||
RetryGap: time.Duration(app.config.Section("advanced").Key("auth_retry_gap").MustInt(10)) * time.Second,
|
RetryGap: time.Duration(app.config.Section("advanced").Key("auth_retry_gap").MustInt(10)) * time.Second,
|
||||||
LogFailures: true,
|
LogFailures: true,
|
||||||
}
|
}
|
||||||
_, err = app.jf.MustAuthenticate(app.config.Section("jellyfin").Key("username").String(), app.config.Section("jellyfin").Key("password").String(), retryOpts)
|
_, status, err = app.jf.MustAuthenticate(app.config.Section("jellyfin").Key("username").String(), app.config.Section("jellyfin").Key("password").String(), retryOpts)
|
||||||
if err != nil {
|
if status != 200 || err != nil {
|
||||||
app.err.Fatalf(lm.FailedAuthJellyfin, server, status, err)
|
app.err.Fatalf(lm.FailedAuthJellyfin, server, status, err)
|
||||||
}
|
}
|
||||||
app.info.Printf(lm.AuthJellyfin, server)
|
app.info.Printf(lm.AuthJellyfin, server)
|
||||||
|
@ -185,14 +185,14 @@ func linkExistingOmbiDiscordTelegram(app *appContext) error {
|
|||||||
idList[user.JellyfinID] = vals
|
idList[user.JellyfinID] = vals
|
||||||
}
|
}
|
||||||
for jfID, ids := range idList {
|
for jfID, ids := range idList {
|
||||||
ombiUser, err := app.getOmbiUser(jfID)
|
ombiUser, status, err := app.getOmbiUser(jfID)
|
||||||
if err != nil {
|
if status != 200 || err != nil {
|
||||||
app.debug.Printf("Failed to get Ombi user with Discord/Telegram \"%s\"/\"%s\": %v", ids[0], ids[1], err)
|
app.debug.Printf("Failed to get Ombi user with Discord/Telegram \"%s\"/\"%s\" (%d): %v", ids[0], ids[1], status, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
_, err = app.ombi.SetNotificationPrefs(ombiUser, ids[0], ids[1])
|
_, status, err = app.ombi.SetNotificationPrefs(ombiUser, ids[0], ids[1])
|
||||||
if err != nil {
|
if status != 200 || err != nil {
|
||||||
app.debug.Printf("Failed to set prefs for Ombi user \"%s\": %v", ombiUser["userName"].(string), err)
|
app.debug.Printf("Failed to set prefs for Ombi user \"%s\" (%d): %v", ombiUser["userName"].(string), status, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,5 +3,3 @@ module github.com/hrfee/jfa-go/ombi
|
|||||||
replace github.com/hrfee/jfa-go/common => ../common
|
replace github.com/hrfee/jfa-go/common => ../common
|
||||||
|
|
||||||
go 1.15
|
go 1.15
|
||||||
|
|
||||||
require github.com/hrfee/jfa-go/common v0.0.0-20240805130902-86c37fb4237b // indirect
|
|
||||||
|
43
ombi/ombi.go
43
ombi/ombi.go
@ -10,7 +10,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
co "github.com/hrfee/jfa-go/common"
|
"github.com/hrfee/jfa-go/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -26,11 +26,11 @@ type Ombi struct {
|
|||||||
userCache []map[string]interface{}
|
userCache []map[string]interface{}
|
||||||
cacheExpiry time.Time
|
cacheExpiry time.Time
|
||||||
cacheLength int
|
cacheLength int
|
||||||
timeoutHandler co.TimeoutHandler
|
timeoutHandler common.TimeoutHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOmbi returns an Ombi object.
|
// NewOmbi returns an Ombi object.
|
||||||
func NewOmbi(server, key string, timeoutHandler co.TimeoutHandler) *Ombi {
|
func NewOmbi(server, key string, timeoutHandler common.TimeoutHandler) *Ombi {
|
||||||
return &Ombi{
|
return &Ombi{
|
||||||
server: server,
|
server: server,
|
||||||
key: key,
|
key: key,
|
||||||
@ -133,19 +133,17 @@ func (ombi *Ombi) put(url string, data map[string]interface{}, response bool) (s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ModifyUser applies the given modified user object to the corresponding user.
|
// ModifyUser applies the given modified user object to the corresponding user.
|
||||||
func (ombi *Ombi) ModifyUser(user map[string]interface{}) (err error) {
|
func (ombi *Ombi) ModifyUser(user map[string]interface{}) (status int, err error) {
|
||||||
if _, ok := user["id"]; !ok {
|
if _, ok := user["id"]; !ok {
|
||||||
err = fmt.Errorf("No ID provided")
|
err = fmt.Errorf("No ID provided")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var status int
|
|
||||||
_, status, err = ombi.put(ombi.server+"/api/v1/Identity/", user, false)
|
_, status, err = ombi.put(ombi.server+"/api/v1/Identity/", user, false)
|
||||||
err = co.GenericErr(status, err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteUser deletes the user corresponding to the given ID.
|
// DeleteUser deletes the user corresponding to the given ID.
|
||||||
func (ombi *Ombi) DeleteUser(id string) (err error) {
|
func (ombi *Ombi) DeleteUser(id string) (code int, err error) {
|
||||||
url := fmt.Sprintf("%s/api/v1/Identity/%s", ombi.server, id)
|
url := fmt.Sprintf("%s/api/v1/Identity/%s", ombi.server, id)
|
||||||
req, _ := http.NewRequest("DELETE", url, nil)
|
req, _ := http.NewRequest("DELETE", url, nil)
|
||||||
req.Header.Add("Content-Type", "application/json")
|
req.Header.Add("Content-Type", "application/json")
|
||||||
@ -154,19 +152,18 @@ func (ombi *Ombi) DeleteUser(id string) (err error) {
|
|||||||
}
|
}
|
||||||
resp, err := ombi.httpClient.Do(req)
|
resp, err := ombi.httpClient.Do(req)
|
||||||
defer ombi.timeoutHandler()
|
defer ombi.timeoutHandler()
|
||||||
return co.GenericErr(resp.StatusCode, err)
|
return resp.StatusCode, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserByID returns the user corresponding to the provided ID.
|
// UserByID returns the user corresponding to the provided ID.
|
||||||
func (ombi *Ombi) UserByID(id string) (result map[string]interface{}, err error) {
|
func (ombi *Ombi) UserByID(id string) (result map[string]interface{}, code int, err error) {
|
||||||
resp, code, err := ombi.getJSON(fmt.Sprintf("%s/api/v1/Identity/User/%s", ombi.server, id), nil)
|
resp, code, err := ombi.getJSON(fmt.Sprintf("%s/api/v1/Identity/User/%s", ombi.server, id), nil)
|
||||||
err = co.GenericErr(code, err)
|
|
||||||
json.Unmarshal([]byte(resp), &result)
|
json.Unmarshal([]byte(resp), &result)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUsers returns all users on the Ombi instance.
|
// GetUsers returns all users on the Ombi instance.
|
||||||
func (ombi *Ombi) GetUsers() ([]map[string]interface{}, error) {
|
func (ombi *Ombi) GetUsers() ([]map[string]interface{}, int, error) {
|
||||||
if time.Now().After(ombi.cacheExpiry) {
|
if time.Now().After(ombi.cacheExpiry) {
|
||||||
resp, code, err := ombi.getJSON(fmt.Sprintf("%s/api/v1/Identity/Users", ombi.server), nil)
|
resp, code, err := ombi.getJSON(fmt.Sprintf("%s/api/v1/Identity/Users", ombi.server), nil)
|
||||||
var result []map[string]interface{}
|
var result []map[string]interface{}
|
||||||
@ -175,10 +172,9 @@ func (ombi *Ombi) GetUsers() ([]map[string]interface{}, error) {
|
|||||||
if (code == 200 || code == 204) && err == nil {
|
if (code == 200 || code == 204) && err == nil {
|
||||||
ombi.cacheExpiry = time.Now().Add(time.Minute * time.Duration(ombi.cacheLength))
|
ombi.cacheExpiry = time.Now().Add(time.Minute * time.Duration(ombi.cacheLength))
|
||||||
}
|
}
|
||||||
err = co.GenericErr(code, err)
|
return result, code, err
|
||||||
return result, err
|
|
||||||
}
|
}
|
||||||
return ombi.userCache, nil
|
return ombi.userCache, 200, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strip these from a user when saving as a template.
|
// Strip these from a user when saving as a template.
|
||||||
@ -194,9 +190,9 @@ var stripFromOmbi = []string{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TemplateByID returns a template based on the user corresponding to the provided ID's settings.
|
// TemplateByID returns a template based on the user corresponding to the provided ID's settings.
|
||||||
func (ombi *Ombi) TemplateByID(id string) (result map[string]interface{}, err error) {
|
func (ombi *Ombi) TemplateByID(id string) (result map[string]interface{}, code int, err error) {
|
||||||
result, err = ombi.UserByID(id)
|
result, code, err = ombi.UserByID(id)
|
||||||
if err != nil {
|
if err != nil || code != 200 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, key := range stripFromOmbi {
|
for _, key := range stripFromOmbi {
|
||||||
@ -213,25 +209,24 @@ func (ombi *Ombi) TemplateByID(id string) (result map[string]interface{}, err er
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewUser creates a new user with the given username, password and email address.
|
// NewUser creates a new user with the given username, password and email address.
|
||||||
func (ombi *Ombi) NewUser(username, password, email string, template map[string]interface{}) ([]string, error) {
|
func (ombi *Ombi) NewUser(username, password, email string, template map[string]interface{}) ([]string, int, error) {
|
||||||
url := fmt.Sprintf("%s/api/v1/Identity", ombi.server)
|
url := fmt.Sprintf("%s/api/v1/Identity", ombi.server)
|
||||||
user := template
|
user := template
|
||||||
user["userName"] = username
|
user["userName"] = username
|
||||||
user["password"] = password
|
user["password"] = password
|
||||||
user["emailAddress"] = email
|
user["emailAddress"] = email
|
||||||
resp, code, err := ombi.post(url, user, true)
|
resp, code, err := ombi.post(url, user, true)
|
||||||
err = co.GenericErr(code, err)
|
|
||||||
var data map[string]interface{}
|
var data map[string]interface{}
|
||||||
json.Unmarshal([]byte(resp), &data)
|
json.Unmarshal([]byte(resp), &data)
|
||||||
if err != nil {
|
if err != nil || code != 200 {
|
||||||
var lst []string
|
var lst []string
|
||||||
if data["errors"] != nil {
|
if data["errors"] != nil {
|
||||||
lst = data["errors"].([]string)
|
lst = data["errors"].([]string)
|
||||||
}
|
}
|
||||||
return lst, err
|
return lst, code, err
|
||||||
}
|
}
|
||||||
ombi.cacheExpiry = time.Now()
|
ombi.cacheExpiry = time.Now()
|
||||||
return nil, err
|
return nil, code, err
|
||||||
}
|
}
|
||||||
|
|
||||||
type NotificationPref struct {
|
type NotificationPref struct {
|
||||||
@ -241,7 +236,7 @@ type NotificationPref struct {
|
|||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ombi *Ombi) SetNotificationPrefs(user map[string]interface{}, discordID, telegramUser string) (result string, err error) {
|
func (ombi *Ombi) SetNotificationPrefs(user map[string]interface{}, discordID, telegramUser string) (result string, code int, err error) {
|
||||||
id := user["id"].(string)
|
id := user["id"].(string)
|
||||||
url := fmt.Sprintf("%s/api/v1/Identity/NotificationPreferences", ombi.server)
|
url := fmt.Sprintf("%s/api/v1/Identity/NotificationPreferences", ombi.server)
|
||||||
data := []NotificationPref{}
|
data := []NotificationPref{}
|
||||||
@ -251,8 +246,6 @@ func (ombi *Ombi) SetNotificationPrefs(user map[string]interface{}, discordID, t
|
|||||||
if telegramUser != "" {
|
if telegramUser != "" {
|
||||||
data = append(data, NotificationPref{NotifAgentTelegram, id, telegramUser, true})
|
data = append(data, NotificationPref{NotifAgentTelegram, id, telegramUser, true})
|
||||||
}
|
}
|
||||||
var code int
|
|
||||||
result, code, err = ombi.send("POST", url, data, true, map[string]string{"UserName": user["userName"].(string)})
|
result, code, err = ombi.send("POST", url, data, true, map[string]string{"UserName": user["userName"].(string)})
|
||||||
err = co.GenericErr(code, err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -14,8 +14,8 @@ import (
|
|||||||
// GenInternalReset generates a local password reset PIN, for use with the PWR option on the Admin page.
|
// GenInternalReset generates a local password reset PIN, for use with the PWR option on the Admin page.
|
||||||
func (app *appContext) GenInternalReset(userID string) (InternalPWR, error) {
|
func (app *appContext) GenInternalReset(userID string) (InternalPWR, error) {
|
||||||
pin := genAuthToken()
|
pin := genAuthToken()
|
||||||
user, err := app.jf.UserByID(userID, false)
|
user, status, err := app.jf.UserByID(userID, false)
|
||||||
if err != nil {
|
if err != nil || status != 200 {
|
||||||
return InternalPWR{}, err
|
return InternalPWR{}, err
|
||||||
}
|
}
|
||||||
pwr := InternalPWR{
|
pwr := InternalPWR{
|
||||||
@ -95,8 +95,8 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) {
|
|||||||
}
|
}
|
||||||
app.info.Printf("New password reset for user \"%s\"", pwr.Username)
|
app.info.Printf("New password reset for user \"%s\"", pwr.Username)
|
||||||
if currentTime := time.Now(); pwr.Expiry.After(currentTime) {
|
if currentTime := time.Now(); pwr.Expiry.After(currentTime) {
|
||||||
user, err := app.jf.UserByName(pwr.Username, false)
|
user, status, err := app.jf.UserByName(pwr.Username, false)
|
||||||
if err != nil || user.ID == "" {
|
if !(status == 200 || status == 204) || err != nil || user.ID == "" {
|
||||||
app.err.Printf(lm.FailedGetUser, pwr.Username, lm.Jellyfin, err)
|
app.err.Printf(lm.FailedGetUser, pwr.Username, lm.Jellyfin, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
25
setup.go
25
setup.go
@ -91,22 +91,19 @@ func (app *appContext) TestJF(gc *gin.Context) {
|
|||||||
tempjf.SetTransport(transport)
|
tempjf.SetTransport(transport)
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := tempjf.Authenticate(req.Username, req.Password)
|
user, status, err := tempjf.Authenticate(req.Username, req.Password)
|
||||||
if err != nil {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
msg := ""
|
msg := ""
|
||||||
status := 500
|
switch status {
|
||||||
switch err.(type) {
|
case 0:
|
||||||
case mediabrowser.ErrUnauthorized:
|
|
||||||
msg = "errorInvalidUserPass"
|
|
||||||
status = 401
|
|
||||||
case mediabrowser.ErrForbidden:
|
|
||||||
msg = "errorUserDisabled"
|
|
||||||
status = 403
|
|
||||||
case mediabrowser.ErrNotFound:
|
|
||||||
msg = "error404"
|
|
||||||
status = 404
|
|
||||||
default:
|
|
||||||
msg = "errorConnectionRefused"
|
msg = "errorConnectionRefused"
|
||||||
|
status = 500
|
||||||
|
case 401:
|
||||||
|
msg = "errorInvalidUserPass"
|
||||||
|
case 403:
|
||||||
|
msg = "errorUserDisabled"
|
||||||
|
case 404:
|
||||||
|
msg = "error404"
|
||||||
}
|
}
|
||||||
app.err.Printf(lm.FailedAuthJellyfin, req.Server, status, err)
|
app.err.Printf(lm.FailedAuthJellyfin, req.Server, status, err)
|
||||||
if msg != "" {
|
if msg != "" {
|
||||||
|
@ -52,7 +52,7 @@ sudo apt-get install jfa-go-tray
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="page-container m-2 lg:my-20 lg:mx-72" id="page-container">
|
<div class="page-container" id="page-container">
|
||||||
<div class="card ~neutral @low mb-1">
|
<div class="card ~neutral @low mb-1">
|
||||||
<div class="row col flex center">
|
<div class="row col flex center">
|
||||||
<span class="heading welcome">jellyfin-accounts (go)</span>
|
<span class="heading welcome">jellyfin-accounts (go)</span>
|
||||||
|
@ -627,8 +627,6 @@ export class settingsList {
|
|||||||
private _saveNoRestart = document.getElementById("settings-apply-no-restart") as HTMLSpanElement;
|
private _saveNoRestart = document.getElementById("settings-apply-no-restart") as HTMLSpanElement;
|
||||||
private _saveRestart = document.getElementById("settings-apply-restart") as HTMLSpanElement;
|
private _saveRestart = document.getElementById("settings-apply-restart") as HTMLSpanElement;
|
||||||
|
|
||||||
private _loader = document.getElementById("settings-loader") as HTMLDivElement;
|
|
||||||
|
|
||||||
private _panel = document.getElementById("settings-panel") as HTMLDivElement;
|
private _panel = document.getElementById("settings-panel") as HTMLDivElement;
|
||||||
private _sidebar = document.getElementById("settings-sidebar") as HTMLDivElement;
|
private _sidebar = document.getElementById("settings-sidebar") as HTMLDivElement;
|
||||||
private _visibleSection: string;
|
private _visibleSection: string;
|
||||||
@ -652,7 +650,7 @@ export class settingsList {
|
|||||||
this._sections[name] = section;
|
this._sections[name] = section;
|
||||||
this._panel.appendChild(this._sections[name].asElement());
|
this._panel.appendChild(this._sections[name].asElement());
|
||||||
const button = document.createElement("span") as HTMLSpanElement;
|
const button = document.createElement("span") as HTMLSpanElement;
|
||||||
button.classList.add("button", "~neutral", "@low", "settings-section-button", "justify-between");
|
button.classList.add("button", "~neutral", "@low", "settings-section-button", "justify-between", "mb-2");
|
||||||
button.textContent = s.meta.name;
|
button.textContent = s.meta.name;
|
||||||
if (subButton) { button.appendChild(subButton); }
|
if (subButton) { button.appendChild(subButton); }
|
||||||
button.onclick = () => { this._showPanel(name); };
|
button.onclick = () => { this._showPanel(name); };
|
||||||
@ -907,73 +905,63 @@ export class settingsList {
|
|||||||
window.modals.matrix.show();
|
window.modals.matrix.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
reload = () => {
|
reload = () => _get("/config", null, (req: XMLHttpRequest) => {
|
||||||
for (let i = 0; i < this._loader.children.length; i++) {
|
if (req.readyState == 4) {
|
||||||
this._loader.children[i].classList.add("invisible");
|
if (req.status != 200) {
|
||||||
}
|
window.notifications.customError("settingsLoadError", window.lang.notif("errorLoadSettings"));
|
||||||
addLoader(this._loader, false, true);
|
return;
|
||||||
_get("/config", null, (req: XMLHttpRequest) => {
|
}
|
||||||
if (req.readyState == 4) {
|
this._settings = req.response as Settings;
|
||||||
if (req.status != 200) {
|
for (let name of this._settings.order) {
|
||||||
window.notifications.customError("settingsLoadError", window.lang.notif("errorLoadSettings"));
|
if (name in this._sections) {
|
||||||
return;
|
this._sections[name].update(this._settings.sections[name]);
|
||||||
}
|
} else {
|
||||||
this._settings = req.response as Settings;
|
if (name == "messages" || name == "user_page") {
|
||||||
for (let name of this._settings.order) {
|
const editButton = document.createElement("div");
|
||||||
if (name in this._sections) {
|
editButton.classList.add("tooltip", "left");
|
||||||
this._sections[name].update(this._settings.sections[name]);
|
editButton.innerHTML = `
|
||||||
} else {
|
<span class="button ~neutral @low">
|
||||||
if (name == "messages" || name == "user_page") {
|
<i class="icon ri-edit-line"></i>
|
||||||
const editButton = document.createElement("div");
|
</span>
|
||||||
editButton.classList.add("tooltip", "left");
|
<span class="content sm">
|
||||||
editButton.innerHTML = `
|
${window.lang.get("strings", "customizeMessages")}
|
||||||
<span class="button ~neutral @low">
|
</span>
|
||||||
<i class="icon ri-edit-line"></i>
|
`;
|
||||||
</span>
|
(editButton.querySelector("span.button") as HTMLSpanElement).onclick = () => {
|
||||||
<span class="content sm">
|
this._messageEditor.showList(name == "messages" ? "email" : "user");
|
||||||
${window.lang.get("strings", "customizeMessages")}
|
};
|
||||||
</span>
|
this.addSection(name, this._settings.sections[name], editButton);
|
||||||
`;
|
} else if (name == "updates") {
|
||||||
(editButton.querySelector("span.button") as HTMLSpanElement).onclick = () => {
|
const icon = document.createElement("span") as HTMLSpanElement;
|
||||||
this._messageEditor.showList(name == "messages" ? "email" : "user");
|
if (window.updater.updateAvailable) {
|
||||||
};
|
icon.classList.add("button", "~urge");
|
||||||
this.addSection(name, this._settings.sections[name], editButton);
|
icon.innerHTML = `<i class="ri-download-line" title="${window.lang.strings("update")}"></i>`;
|
||||||
} else if (name == "updates") {
|
icon.onclick = () => window.updater.checkForUpdates(window.modals.updateInfo.show);
|
||||||
const icon = document.createElement("span") as HTMLSpanElement;
|
|
||||||
if (window.updater.updateAvailable) {
|
|
||||||
icon.classList.add("button", "~urge");
|
|
||||||
icon.innerHTML = `<i class="ri-download-line" title="${window.lang.strings("update")}"></i>`;
|
|
||||||
icon.onclick = () => window.updater.checkForUpdates(window.modals.updateInfo.show);
|
|
||||||
}
|
|
||||||
this.addSection(name, this._settings.sections[name], icon);
|
|
||||||
} else if (name == "matrix" && !window.matrixEnabled) {
|
|
||||||
const addButton = document.createElement("div");
|
|
||||||
addButton.classList.add("tooltip", "left");
|
|
||||||
addButton.innerHTML = `
|
|
||||||
<span class="button ~neutral @low">+</span>
|
|
||||||
<span class="content sm">
|
|
||||||
${window.lang.strings("linkMatrix")}
|
|
||||||
</span>
|
|
||||||
`;
|
|
||||||
(addButton.querySelector("span.button") as HTMLSpanElement).onclick = this._addMatrix;
|
|
||||||
this.addSection(name, this._settings.sections[name], addButton);
|
|
||||||
} else {
|
|
||||||
this.addSection(name, this._settings.sections[name]);
|
|
||||||
}
|
}
|
||||||
|
this.addSection(name, this._settings.sections[name], icon);
|
||||||
|
} else if (name == "matrix" && !window.matrixEnabled) {
|
||||||
|
const addButton = document.createElement("div");
|
||||||
|
addButton.classList.add("tooltip", "left");
|
||||||
|
addButton.innerHTML = `
|
||||||
|
<span class="button ~neutral @low">+</span>
|
||||||
|
<span class="content sm">
|
||||||
|
${window.lang.strings("linkMatrix")}
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
(addButton.querySelector("span.button") as HTMLSpanElement).onclick = this._addMatrix;
|
||||||
|
this.addSection(name, this._settings.sections[name], addButton);
|
||||||
|
} else {
|
||||||
|
this.addSection(name, this._settings.sections[name]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
removeLoader(this._loader);
|
|
||||||
for (let i = 0; i < this._loader.children.length; i++) {
|
|
||||||
this._loader.children[i].classList.remove("invisible");
|
|
||||||
}
|
|
||||||
this._showPanel(this._settings.order[0]);
|
|
||||||
document.dispatchEvent(new CustomEvent("settings-loaded"));
|
|
||||||
document.dispatchEvent(new CustomEvent("settings-advancedState", { detail: false }));
|
|
||||||
this._saveButton.classList.add("unfocused");
|
|
||||||
this._needsRestart = false;
|
|
||||||
}
|
}
|
||||||
})
|
this._showPanel(this._settings.order[0]);
|
||||||
};
|
document.dispatchEvent(new CustomEvent("settings-loaded"));
|
||||||
|
document.dispatchEvent(new CustomEvent("settings-advancedState", { detail: false }));
|
||||||
|
this._saveButton.classList.add("unfocused");
|
||||||
|
this._needsRestart = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// FIXME: Search "About" & "User profiles", pseudo-search "User profiles" for things like "Ombi", "Referrals", etc.
|
// FIXME: Search "About" & "User profiles", pseudo-search "User profiles" for things like "Ombi", "Referrals", etc.
|
||||||
search = (query: string) => {
|
search = (query: string) => {
|
||||||
|
@ -24,8 +24,8 @@ func (app *appContext) checkUsers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.info.Println(lm.CheckUserExpiries)
|
app.info.Println(lm.CheckUserExpiries)
|
||||||
users, err := app.jf.GetUsers(false)
|
users, status, err := app.jf.GetUsers(false)
|
||||||
if err != nil {
|
if err != nil || status != 200 {
|
||||||
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -89,7 +89,7 @@ func (app *appContext) checkUsers() {
|
|||||||
err, _, _ = app.SetUserDisabled(user, true)
|
err, _, _ = app.SetUserDisabled(user, true)
|
||||||
activity.Type = ActivityDisabled
|
activity.Type = ActivityDisabled
|
||||||
}
|
}
|
||||||
if err != nil {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
app.err.Printf(lm.FailedDeleteOrDisableExpiredUser, user.ID, err)
|
app.err.Printf(lm.FailedDeleteOrDisableExpiredUser, user.ID, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
41
users.go
41
users.go
@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@ -67,7 +68,7 @@ func (app *appContext) NewUserPostVerification(p NewUserParams) (out NewUserData
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
existingUser, _ := app.jf.UserByName(p.Req.Username, false)
|
existingUser, _, _ := app.jf.UserByName(p.Req.Username, false)
|
||||||
if existingUser.Name != "" {
|
if existingUser.Name != "" {
|
||||||
out.Message = lm.UserExists
|
out.Message = lm.UserExists
|
||||||
deferLogInfo(lm.FailedCreateUser, lm.Jellyfin, p.Req.Username, out.Message)
|
deferLogInfo(lm.FailedCreateUser, lm.Jellyfin, p.Req.Username, out.Message)
|
||||||
@ -75,9 +76,10 @@ func (app *appContext) NewUserPostVerification(p NewUserParams) (out NewUserData
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var status int
|
||||||
var err error
|
var err error
|
||||||
out.User, err = app.jf.NewUser(p.Req.Username, p.Req.Password)
|
out.User, status, err = app.jf.NewUser(p.Req.Username, p.Req.Password)
|
||||||
if err != nil {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
out.Message = err.Error()
|
out.Message = err.Error()
|
||||||
deferLogError(lm.FailedCreateUser, lm.Jellyfin, p.Req.Username, out.Message)
|
deferLogError(lm.FailedCreateUser, lm.Jellyfin, p.Req.Username, out.Message)
|
||||||
out.Status = 401
|
out.Status = 401
|
||||||
@ -98,15 +100,15 @@ func (app *appContext) NewUserPostVerification(p NewUserParams) (out NewUserData
|
|||||||
}, p.ContextForIPLogging, (p.SourceType != ActivityAdmin))
|
}, p.ContextForIPLogging, (p.SourceType != ActivityAdmin))
|
||||||
|
|
||||||
if p.Profile != nil {
|
if p.Profile != nil {
|
||||||
err = app.jf.SetPolicy(out.User.ID, p.Profile.Policy)
|
status, err = app.jf.SetPolicy(out.User.ID, p.Profile.Policy)
|
||||||
if err != nil {
|
if !((status == 200 || status == 204) && err == nil) {
|
||||||
app.err.Printf(lm.FailedApplyTemplate, "policy", lm.Jellyfin, out.User.ID, err)
|
app.err.Printf(lm.FailedApplyTemplate, "policy", lm.Jellyfin, out.User.ID, err)
|
||||||
}
|
}
|
||||||
err = app.jf.SetConfiguration(out.User.ID, p.Profile.Configuration)
|
status, err = app.jf.SetConfiguration(out.User.ID, p.Profile.Configuration)
|
||||||
if err == nil {
|
if (status == 200 || status == 204) && err == nil {
|
||||||
err = app.jf.SetDisplayPreferences(out.User.ID, p.Profile.Displayprefs)
|
status, err = app.jf.SetDisplayPreferences(out.User.ID, p.Profile.Displayprefs)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if !((status == 200 || status == 204) && err == nil) {
|
||||||
app.err.Printf(lm.FailedApplyTemplate, "configuration", lm.Jellyfin, out.User.ID, err)
|
app.err.Printf(lm.FailedApplyTemplate, "configuration", lm.Jellyfin, out.User.ID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,7 +164,11 @@ func (app *appContext) SetUserDisabled(user mediabrowser.User, disabled bool) (e
|
|||||||
change = user.Policy.IsDisabled != disabled
|
change = user.Policy.IsDisabled != disabled
|
||||||
user.Policy.IsDisabled = disabled
|
user.Policy.IsDisabled = disabled
|
||||||
|
|
||||||
err = app.jf.SetPolicy(user.ID, user.Policy)
|
var status int
|
||||||
|
status, err = app.jf.SetPolicy(user.ID, user.Policy)
|
||||||
|
if !(status == 200 || status == 204) && err == nil {
|
||||||
|
err = fmt.Errorf("failed (code %d)", status)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -179,12 +185,16 @@ func (app *appContext) SetUserDisabled(user mediabrowser.User, disabled bool) (e
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) DeleteUser(user mediabrowser.User) (err error, deleted bool) {
|
func (app *appContext) DeleteUser(user mediabrowser.User) (err error, deleted bool) {
|
||||||
|
var status int
|
||||||
if app.ombi != nil {
|
if app.ombi != nil {
|
||||||
var tpUser map[string]any
|
var tpUser map[string]any
|
||||||
tpUser, err = app.getOmbiUser(user.ID)
|
tpUser, status, err = app.getOmbiUser(user.ID)
|
||||||
if err == nil {
|
if status == 200 && err == nil {
|
||||||
if id, ok := tpUser["id"]; ok {
|
if id, ok := tpUser["id"]; ok {
|
||||||
err = app.ombi.DeleteUser(id.(string))
|
status, err = app.ombi.DeleteUser(id.(string))
|
||||||
|
if status != 200 && err == nil {
|
||||||
|
err = fmt.Errorf("failed (code %d)", status)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedDeleteUser, lm.Ombi, user.ID, err)
|
app.err.Printf(lm.FailedDeleteUser, lm.Ombi, user.ID, err)
|
||||||
}
|
}
|
||||||
@ -201,7 +211,10 @@ func (app *appContext) DeleteUser(user mediabrowser.User) (err error, deleted bo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = app.jf.DeleteUser(user.ID)
|
status, err = app.jf.DeleteUser(user.ID)
|
||||||
|
if status != 200 && status != 204 && err == nil {
|
||||||
|
err = fmt.Errorf("failed (code %d)", status)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
36
views.go
36
views.go
@ -17,7 +17,6 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/golang-jwt/jwt"
|
"github.com/golang-jwt/jwt"
|
||||||
"github.com/gomarkdown/markdown"
|
"github.com/gomarkdown/markdown"
|
||||||
"github.com/hrfee/jfa-go/common"
|
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
lm "github.com/hrfee/jfa-go/logmessages"
|
||||||
"github.com/hrfee/mediabrowser"
|
"github.com/hrfee/mediabrowser"
|
||||||
"github.com/lithammer/shortuuid/v3"
|
"github.com/lithammer/shortuuid/v3"
|
||||||
@ -334,25 +333,26 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
|
|||||||
// data["pin"] = pin
|
// data["pin"] = pin
|
||||||
// }
|
// }
|
||||||
var resp mediabrowser.PasswordResetResponse
|
var resp mediabrowser.PasswordResetResponse
|
||||||
|
var status int
|
||||||
var err error
|
var err error
|
||||||
var username string
|
var username string
|
||||||
if !isInternal && !setPassword {
|
if !isInternal && !setPassword {
|
||||||
resp, err = app.jf.ResetPassword(pin)
|
resp, status, err = app.jf.ResetPassword(pin)
|
||||||
} else if time.Now().After(pwr.Expiry) {
|
} else if time.Now().After(pwr.Expiry) {
|
||||||
app.debug.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", fmt.Sprintf(lm.ExpiredPIN, pin))
|
app.debug.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", fmt.Sprintf(lm.ExpiredPIN, pin))
|
||||||
app.NoRouteHandler(gc)
|
app.NoRouteHandler(gc)
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
err = app.jf.ResetPasswordAdmin(pwr.ID)
|
status, err = app.jf.ResetPasswordAdmin(pwr.ID)
|
||||||
if err != nil {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", err)
|
app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", err)
|
||||||
} else {
|
} else {
|
||||||
err = app.jf.SetPassword(pwr.ID, "", pin)
|
status, err = app.jf.SetPassword(pwr.ID, "", pin)
|
||||||
}
|
}
|
||||||
username = pwr.Username
|
username = pwr.Username
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == nil && (isInternal || resp.Success) {
|
if (status == 200 || status == 204) && err == nil && (isInternal || resp.Success) {
|
||||||
data["success"] = true
|
data["success"] = true
|
||||||
data["pin"] = pin
|
data["pin"] = pin
|
||||||
if !isInternal {
|
if !isInternal {
|
||||||
@ -364,8 +364,8 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
|
|||||||
|
|
||||||
// Only log PWRs we know the user for.
|
// Only log PWRs we know the user for.
|
||||||
if username != "" {
|
if username != "" {
|
||||||
jfUser, err := app.jf.UserByName(username, false)
|
jfUser, status, err := app.jf.UserByName(username, false)
|
||||||
if err == nil {
|
if err == nil && status == 200 {
|
||||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||||
Type: ActivityResetPassword,
|
Type: ActivityResetPassword,
|
||||||
UserID: jfUser.ID,
|
UserID: jfUser.ID,
|
||||||
@ -377,19 +377,19 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||||
jfUser, err := app.jf.UserByName(username, false)
|
jfUser, status, err := app.jf.UserByName(username, false)
|
||||||
if err != nil {
|
if status != 200 || err != nil {
|
||||||
app.err.Printf(lm.FailedGetUser, username, lm.Jellyfin, err)
|
app.err.Printf(lm.FailedGetUser, username, lm.Jellyfin, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ombiUser, err := app.getOmbiUser(jfUser.ID)
|
ombiUser, status, err := app.getOmbiUser(jfUser.ID)
|
||||||
if err != nil {
|
if status != 200 || err != nil {
|
||||||
app.err.Printf(lm.FailedGetUser, username, lm.Ombi, err)
|
app.err.Printf(lm.FailedGetUser, username, lm.Ombi, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ombiUser["password"] = pin
|
ombiUser["password"] = pin
|
||||||
err = app.ombi.ModifyUser(ombiUser)
|
status, err = app.ombi.ModifyUser(ombiUser)
|
||||||
if err != nil {
|
if status != 200 || err != nil {
|
||||||
app.err.Printf(lm.FailedChangePassword, lm.Ombi, ombiUser["userName"], err)
|
app.err.Printf(lm.FailedChangePassword, lm.Ombi, ombiUser["userName"], err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -539,7 +539,9 @@ func (app *appContext) verifyCaptcha(code, id, text string, isPWR bool) bool {
|
|||||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
err = common.GenericErr(resp.StatusCode, err)
|
if err == nil && resp.StatusCode != 200 {
|
||||||
|
err = fmt.Errorf("failed (error %d)", resp.StatusCode)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedVerifyReCAPTCHA, err)
|
app.err.Printf(lm.FailedVerifyReCAPTCHA, err)
|
||||||
return false
|
return false
|
||||||
@ -747,8 +749,8 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
|||||||
|
|
||||||
fromUser := ""
|
fromUser := ""
|
||||||
if invite.ReferrerJellyfinID != "" {
|
if invite.ReferrerJellyfinID != "" {
|
||||||
sender, err := app.jf.UserByID(invite.ReferrerJellyfinID, false)
|
sender, status, err := app.jf.UserByID(invite.ReferrerJellyfinID, false)
|
||||||
if err == nil {
|
if status == 200 && err == nil {
|
||||||
fromUser = sender.Name
|
fromUser = sender.Name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user