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

Compare commits

...

7 Commits

Author SHA1 Message Date
6308db495a
build: push redoc after build 2024-08-06 21:06:06 +01:00
ef7132bf3d
build: de-dupe goreleaser
I don't really know why I duplicated the build process in
.goreleaser.yaml, when I could have just called the Makefile.
2024-08-06 20:47:54 +01:00
790accc007
build: Update dockerfile for new Makefile
added a "precompile" step which takes the place of the "configuration
email swager..." stuff.
2024-08-06 20:34:28 +01:00
2310130e6b
api clients: return data, error, no status
jellyseerr already did this, but it's been standardised a little more.

Mediabrowser uses it's own genericErr function and error types due to
being a separate package, while jellyseerr and ombi now share errors
defined in common/.
2024-08-06 14:48:31 +01:00
284312713c
web: css adjustments
tailwind classes define what "page-container" previously did, with some
changes. Logout button moved to top of screen.
2024-08-05 21:18:32 +01:00
b40211a6e0
settings: add loader, improve css a bit
Loader appears on about/user profiles area. These two are now shown next
to each other.
on small screens, the sidebar list of sections is displayed without a
card around it, and the top left/right corner buttons on all pages are
correctly aligned with the content.
2024-08-05 20:23:10 +01:00
ce6a5772b1
build: rewrite Makefile properly (incremental builds)
Probably still a little rough around the edges, but supports the actual
use case of GMake, which I believe are called incremental builds. Builds
will only occur when the code is changed, and only the necessary bits
are re-compiled.
2024-08-05 18:46:21 +01:00
42 changed files with 575 additions and 550 deletions

View File

@ -8,46 +8,8 @@ release:
name_template: "v{{.Version}}"
before:
hooks:
- go mod download
- 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
- npm i
- make precompile INTERNAL=on
builds:
- id: notray
dir: ./

View File

@ -12,14 +12,6 @@ clone:
depth: 0
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
image: docker.io/hrfee/jfa-go-build-docker:latest
environment:
@ -30,6 +22,14 @@ steps:
- curl -sfL https://goreleaser.com/static/run > goreleaser
- chmod +x goreleaser
- ./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
image: docker.io/hrfee/jfa-go-build-docker:latest
environment:

View File

@ -1,6 +1,6 @@
# Use this instead if hrfee/jfa-go-build-docker doesn't support your architecture
# FROM --platform=$BUILDPLATFORM golang:latest AS support
FROM --platform=$BUILDPLATFORM hrfee/jfa-go-build-docker AS support
FROM --platform=$BUILDPLATFORM docker.io/hrfee/jfa-go-build-docker:latest AS support
COPY . /opt/build
@ -9,10 +9,10 @@ COPY . /opt/build
# && apt-get install build-essential python3-pip -y \
# && (curl -sL https://deb.nodesource.com/setup_current.x | bash -) \
# && apt-get install nodejs
RUN (cd /opt/build; make configuration npm email typescript variants-html bundle-css inline-css swagger copy INTERNAL=off GOESBUILD=on) \
RUN (cd /opt/build; npm i; make precompile INTERNAL=off GOESBUILD=off) \
&& 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 golang:latest AS build
FROM --platform=$BUILDPLATFORM docker.io/golang:latest AS build
ARG TARGETARCH
ENV GOARCH=$TARGETARCH
ARG BUILT_BY
@ -30,5 +30,3 @@ EXPOSE 8056
EXPOSE 8057
CMD [ "/opt/jfa-go/jfa-go", "-data", "/data" ]

145
Makefile
View File

@ -1,3 +1,7 @@
.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
ifeq ($(GOESBUILD), on)
ESBUILD := esbuild
@ -7,6 +11,7 @@ endif
GOBINARY ?= go
CSSVERSION ?= v3
CSS_BUNDLE = $(DATA)/web/css/$(CSSVERSION)bundle.css
VERSION ?= $(shell git describe --exact-match HEAD 2> /dev/null || echo vgit)
VERSION := $(shell echo $(VERSION) | sed 's/v//g')
@ -57,7 +62,7 @@ ifeq ($(DEBUG), on)
TYPECHECK := npx tsc -noEmit --project ts/tsconfig.json
# jank
COPYTS := rm -r $(DATA)/web/js/ts; cp -r tempts $(DATA)/web/js/ts
UNCSS := cp $(DATA)/web/css/bundle.css $(DATA)/bundle.css
UNCSS := cp $(CSS_BUNDLE) $(DATA)/bundle.css
# TAILWIND := --content ""
else
LDFLAGS := -s -w $(LDFLAGS)
@ -65,7 +70,7 @@ else
MINIFY := --minify
COPYTS :=
TYPECHECK :=
UNCSS := npx tailwindcss -i $(DATA)/web/css/bundle.css -o $(DATA)/bundle.css --content "html/crash.html"
UNCSS := npx tailwindcss -i $(CSS_BUNDLE) -o $(DATA)/bundle.css --content "html/crash.html"
# UNCSS := npx uncss $(DATA)/crash.html --csspath web/css --output $(DATA)/bundle.css
TAILWIND :=
endif
@ -96,77 +101,104 @@ else
SWAGINSTALL :=
endif
npm:
$(info installing npm dependencies)
npm install $(NPMOPTS)
CONFIG_BASE = config/config-base.json
configuration:
CONFIG_DESCRIPTION = $(DATA)/config-base.json
CONFIG_DEFAULT = $(DATA)/config-default.ini
$(CONFIG_DESCRIPTION) &: $(CONFIG_BASE)
$(info Fixing config-base)
-mkdir -p $(DATA)
python3 scripts/enumerate_config.py -i config/config-base.json -o $(DATA)/config-base.json
$(CONFIG_DEFAULT) &: $(CONFIG_BASE)
$(info Generating config-default.ini)
python3 scripts/generate_ini.py -i config/config-base.json -o $(DATA)/config-default.ini
email:
configuration: $(CONFIG_DESCRIPTION) $(CONFIG_DEFAULT)
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)
python3 scripts/compile_mjml.py -o $(DATA)/
typescript:
TYPESCRIPT_FULLSRC = $(shell find ts/ -type f -name "*.ts")
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)
$(adding dark variants to typescript)
rm -rf tempts
cp -r ts tempts
$(adding dark variants to typescript)
scripts/dark-variant.sh tempts
scripts/dark-variant.sh tempts/modules
$(info compiling typescript)
mkdir -p $(DATA)/web/js
$(ESBUILD) --target=es6 --bundle tempts/admin.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/admin.js $(MINIFY)
$(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)
$(foreach tempsrc,$(TYPESCRIPT_TEMPSRC),$(ESBUILD) --target=es6 --bundle $(tempsrc) $(SOURCEMAP) --outfile=$(patsubst %.ts,%.js,$(subst tempts/,./$(DATA)/web/js/,$(tempsrc))) $(MINIFY);)
mv $(DATA)/web/js/crash.js $(DATA)/
$(COPYTS)
swagger:
SWAGGER_SRC = $(wildcard api*.go) $(wildcard *auth.go) views.go
SWAGGER_TARGET = docs/docs.go
$(SWAGGER_TARGET): $(SWAGGER_SRC)
$(SWAGINSTALL)
swag init -g main.go
compile:
$(info Downloading deps)
$(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:
VARIANTS_SRC = $(wildcard html/*.html)
VARIANTS_TARGET = $(DATA)/html/admin.html
$(VARIANTS_TARGET): $(VARIANTS_SRC)
$(info copying html)
cp -r html $(DATA)/
$(info adding dark variants to html)
node scripts/missing-colors.js html $(DATA)/html
copy:
ICON_SRC = node_modules/remixicon/fonts/remixicon.css node_modules/remixicon/fonts/remixicon.woff2
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)
mv $(DATA)/crash.html $(DATA)/html/
cp $(DATA)/crash.html $(DATA)/html/
$(info copying static data)
mkdir -p $(DATA)/web
cp images/banner.svg static/banner.svg
@ -176,7 +208,22 @@ copy:
$(info copying language files)
cp -r lang $(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:
# python3 scripts/embed.py internal
@ -197,6 +244,6 @@ clean:
-rm docs/docs.go docs/swagger.json docs/swagger.yaml
go clean
quick: configuration typescript variants-html bundle-css inline-css copy compile
all: configuration npm email typescript variants-html bundle-css inline-css swagger copy compile
npm:
$(info installing npm dependencies)
npm install $(NPMOPTS)

View File

@ -144,13 +144,13 @@ func (app *appContext) GetActivities(gc *gin.Context) {
if act.Type == ActivityDeletion || act.Type == ActivityCreation {
resp.Activities[i].Username = act.Value
resp.Activities[i].Value = ""
} else if user, status, err := app.jf.UserByID(act.UserID, false); status == 200 && err == nil {
} else if user, err := app.jf.UserByID(act.UserID, false); err == nil {
resp.Activities[i].Username = user.Name
}
if (act.SourceType == ActivityUser || act.SourceType == ActivityAdmin) && act.Source != "" {
user, status, err := app.jf.UserByID(act.Source, false)
if status == 200 && err == nil {
user, err := app.jf.UserByID(act.Source, false)
if err == nil {
resp.Activities[i].SourceUsername = user.Name
}
}

View File

@ -1,34 +1,35 @@
package main
import (
"errors"
"fmt"
"net/url"
"strings"
"github.com/gin-gonic/gin"
"github.com/hrfee/jfa-go/common"
lm "github.com/hrfee/jfa-go/logmessages"
"github.com/hrfee/jfa-go/ombi"
"github.com/hrfee/mediabrowser"
)
func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, int, error) {
jfUser, code, err := app.jf.UserByID(jfID, false)
if err != nil || code != 200 {
return nil, code, err
func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, error) {
jfUser, err := app.jf.UserByID(jfID, false)
if err != nil {
return nil, err
}
username := jfUser.Name
email := ""
if e, ok := app.storage.GetEmailsKey(jfID); ok {
email = e.Addr
}
return app.ombi.getUser(username, email)
user, err := app.ombi.getUser(username, email)
return user, err
}
func (ombi *OmbiWrapper) getUser(username string, email string) (map[string]interface{}, int, error) {
ombiUsers, code, err := ombi.GetUsers()
if err != nil || code != 200 {
return nil, code, err
func (ombi *OmbiWrapper) getUser(username string, email string) (map[string]interface{}, error) {
ombiUsers, err := ombi.GetUsers()
if err != nil {
return nil, err
}
for _, ombiUser := range ombiUsers {
ombiAddr := ""
@ -36,18 +37,19 @@ func (ombi *OmbiWrapper) getUser(username string, email string) (map[string]inte
ombiAddr = a.(string)
}
if ombiUser["userName"].(string) == username || (ombiAddr == email && email != "") {
return ombiUser, code, err
return ombiUser, err
}
}
return nil, 400, errors.New(lm.NotFound)
// Gets a generic "not found" type error
return nil, common.GenericErr(404, err)
}
// 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{}, int, error) {
func (ombi *OmbiWrapper) getImportedUser(name string) (map[string]interface{}, error) {
// Ombi User Types: 3/4 = Emby, 5 = Jellyfin
ombiUsers, code, err := ombi.GetUsers()
if err != nil || code != 200 {
return nil, code, err
ombiUsers, err := ombi.GetUsers()
if err != nil {
return nil, err
}
for _, ombiUser := range ombiUsers {
if ombiUser["userName"].(string) == name {
@ -60,10 +62,11 @@ func (ombi *OmbiWrapper) getImportedUser(name string) (map[string]interface{}, i
} else if uType != 3 && uType != 4 { // Emby
continue
}
return ombiUser, code, err
return ombiUser, err
}
}
return nil, 400, fmt.Errorf("couldn't find user")
// Gets a generic "not found" type error
return nil, common.GenericErr(404, err)
}
// @Summary Get a list of Ombi users.
@ -74,8 +77,8 @@ func (ombi *OmbiWrapper) getImportedUser(name string) (map[string]interface{}, i
// @Security Bearer
// @tags Ombi
func (app *appContext) OmbiUsers(gc *gin.Context) {
users, status, err := app.ombi.GetUsers()
if err != nil || status != 200 {
users, err := app.ombi.GetUsers()
if err != nil {
app.err.Printf(lm.FailedGetUsers, lm.Ombi, err)
respond(500, "Couldn't get users", gc)
return
@ -110,8 +113,8 @@ func (app *appContext) SetOmbiProfile(gc *gin.Context) {
respondBool(400, false, gc)
return
}
template, code, err := app.ombi.TemplateByID(req.ID)
if err != nil || code != 200 || len(template) == 0 {
template, err := app.ombi.TemplateByID(req.ID)
if err != nil || len(template) == 0 {
app.err.Printf(lm.FailedGetUsers, lm.Ombi, err)
respond(500, "Couldn't get user", gc)
return
@ -147,7 +150,7 @@ type OmbiWrapper struct {
*ombi.Ombi
}
func (ombi *OmbiWrapper) applyProfile(user map[string]interface{}, profile map[string]interface{}) (status int, err error) {
func (ombi *OmbiWrapper) applyProfile(user map[string]interface{}, profile map[string]interface{}) (err error) {
for k, v := range profile {
switch v.(type) {
case map[string]interface{}, []interface{}:
@ -158,22 +161,21 @@ func (ombi *OmbiWrapper) applyProfile(user map[string]interface{}, profile map[s
}
}
}
status, err = ombi.ModifyUser(user)
err = ombi.ModifyUser(user)
return
}
func (ombi *OmbiWrapper) ImportUser(jellyfinID string, req newUserDTO, profile Profile) (err error, ok bool) {
errors, code, err := ombi.NewUser(req.Username, req.Password, req.Email, profile.Ombi)
errors, err := ombi.NewUser(req.Username, req.Password, req.Email, profile.Ombi)
var ombiUser map[string]interface{}
var status int
if err != nil || code != 200 {
if err != nil {
// Check if on the off chance, Ombi's user importer has already added the account.
ombiUser, status, err = ombi.getImportedUser(req.Username)
if status == 200 && err == nil {
ombiUser, err = ombi.getImportedUser(req.Username)
if err == nil {
// app.info.Println(lm.Ombi + " " + lm.UserExists)
profile.Ombi["password"] = req.Password
status, err = ombi.applyProfile(ombiUser, profile.Ombi)
if status != 200 || err != nil {
err = ombi.applyProfile(ombiUser, profile.Ombi)
if err != nil {
err = fmt.Errorf(lm.FailedApplyProfile, lm.Ombi, req.Username, err)
}
} else {
@ -189,9 +191,8 @@ func (ombi *OmbiWrapper) ImportUser(jellyfinID string, req newUserDTO, profile P
func (ombi *OmbiWrapper) AddContactMethods(jellyfinID string, req newUserDTO, discord *DiscordUser, telegram *TelegramUser) (err error) {
var ombiUser map[string]interface{}
var status int
ombiUser, status, err = ombi.getUser(req.Username, req.Email)
if status != 200 || err != nil {
ombiUser, err = ombi.getUser(req.Username, req.Email)
if err != nil {
return
}
if discordEnabled || telegramEnabled {
@ -204,9 +205,8 @@ func (ombi *OmbiWrapper) AddContactMethods(jellyfinID string, req newUserDTO, di
tUser = telegram.Username
}
var resp string
var status int
resp, status, err = ombi.SetNotificationPrefs(ombiUser, dID, tUser)
if !(status == 200 || status == 204) || err != nil {
resp, err = ombi.SetNotificationPrefs(ombiUser, dID, tUser)
if err != nil {
if resp != "" {
err = fmt.Errorf("%v, %s", err, resp)
}

View File

@ -84,8 +84,8 @@ func (app *appContext) CreateProfile(gc *gin.Context) {
var req newProfileDTO
gc.BindJSON(&req)
app.jf.CacheExpiry = time.Now()
user, status, err := app.jf.UserByID(req.ID, false)
if !(status == 200 || status == 204) || err != nil {
user, err := app.jf.UserByID(req.ID, false)
if err != nil {
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
respond(500, "Couldn't get user", gc)
return
@ -98,8 +98,8 @@ func (app *appContext) CreateProfile(gc *gin.Context) {
app.debug.Printf(lm.CreateProfileFromUser, user.Name)
if req.Homescreen {
profile.Configuration = user.Configuration
profile.Displayprefs, status, err = app.jf.GetDisplayPreferences(req.ID)
if !(status == 200 || status == 204) || err != nil {
profile.Displayprefs, err = app.jf.GetDisplayPreferences(req.ID)
if err != nil {
app.err.Printf(lm.FailedGetJellyfinDisplayPrefs, req.ID, err)
respond(500, "Couldn't get displayprefs", gc)
return

View File

@ -29,8 +29,8 @@ func (app *appContext) MyDetails(gc *gin.Context) {
Id: gc.GetString("jfId"),
}
user, status, err := app.jf.UserByID(resp.Id, false)
if status != 200 || err != nil {
user, err := app.jf.UserByID(resp.Id, false)
if err != nil {
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
respond(500, "Failed to get user", gc)
return
@ -259,9 +259,9 @@ func (app *appContext) ModifyMyEmail(gc *gin.Context) {
}
if emailEnabled && app.config.Section("email_confirmation").Key("enabled").MustBool(false) {
user, status, err := app.jf.UserByID(id, false)
user, err := app.jf.UserByID(id, false)
name := ""
if status == 200 && err == nil {
if err == nil {
name = user.Name
}
app.debug.Printf(lm.EmailConfirmationRequired, id)
@ -688,20 +688,20 @@ func (app *appContext) ChangeMyPassword(gc *gin.Context) {
return
}
}
user, status, err := app.jf.UserByID(gc.GetString("jfId"), false)
if status != 200 || err != nil {
user, err := app.jf.UserByID(gc.GetString("jfId"), false)
if err != nil {
app.err.Printf(lm.FailedGetUser, gc.GetString("jfId"), lm.Jellyfin, err)
respondBool(500, false, gc)
return
}
// Authenticate as user to confirm old password.
user, status, err = app.authJf.Authenticate(user.Name, req.Old)
if status != 200 || err != nil {
user, err = app.authJf.Authenticate(user.Name, req.Old)
if err != nil {
respondBool(401, false, gc)
return
}
status, err = app.jf.SetPassword(gc.GetString("jfId"), req.Old, req.New)
if (status != 200 && status != 204) || err != nil {
err = app.jf.SetPassword(gc.GetString("jfId"), req.Old, req.New)
if err != nil {
respondBool(500, false, gc)
return
}
@ -716,14 +716,14 @@ func (app *appContext) ChangeMyPassword(gc *gin.Context) {
if app.config.Section("ombi").Key("enabled").MustBool(false) {
func() {
ombiUser, status, err := app.getOmbiUser(gc.GetString("jfId"))
if status != 200 || err != nil {
ombiUser, err := app.getOmbiUser(gc.GetString("jfId"))
if err != nil {
app.err.Printf(lm.FailedGetUser, user.Name, lm.Ombi, err)
return
}
ombiUser["password"] = req.New
status, err = app.ombi.ModifyUser(ombiUser)
if status != 200 || err != nil {
err = app.ombi.ModifyUser(ombiUser)
if err != nil {
app.err.Printf(lm.FailedChangePassword, lm.Ombi, ombiUser["userName"], err)
return
}

View File

@ -379,9 +379,9 @@ func (app *appContext) EnableDisableUsers(gc *gin.Context) {
activityType = ActivityEnabled
}
for _, userID := range req.Users {
user, status, err := app.jf.UserByID(userID, false)
if status != 200 || err != nil {
errors["GetUser"][user.ID] = fmt.Sprintf("%d %v", status, err)
user, err := app.jf.UserByID(userID, false)
if err != nil {
errors["GetUser"][user.ID] = err.Error()
app.err.Printf(lm.FailedGetUser, user.ID, lm.Jellyfin, err)
continue
}
@ -440,10 +440,7 @@ func (app *appContext) DeleteUsers(gc *gin.Context) {
}
}
for _, userID := range req.Users {
user, status, err := app.jf.UserByID(userID, false)
if status != 200 && err == nil {
err = fmt.Errorf("failed (code %d)", status)
}
user, err := app.jf.UserByID(userID, false)
if err != nil {
app.err.Printf(lm.FailedGetUser, user.ID, lm.Jellyfin, err)
errors[userID] = err.Error()
@ -502,7 +499,6 @@ func (app *appContext) DeleteUsers(gc *gin.Context) {
func (app *appContext) ExtendExpiry(gc *gin.Context) {
var req extendExpiryDTO
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 {
respondBool(400, false, gc)
return
@ -522,8 +518,8 @@ func (app *appContext) ExtendExpiry(gc *gin.Context) {
app.storage.SetUserExpiryKey(id, expiry)
if messagesEnabled && req.Notify {
go func(uid string, exp time.Time) {
user, status, err := app.jf.UserByID(uid, false)
if status != 200 || err != nil {
user, err := app.jf.UserByID(uid, false)
if err != nil {
return
}
msg, err := app.email.constructExpiryAdjusted(user.Name, exp, req.Reason, app, false)
@ -655,8 +651,8 @@ func (app *appContext) Announce(gc *gin.Context) {
unique := strings.Contains(req.Message, "{username}")
if unique {
for _, userID := range req.Users {
user, status, err := app.jf.UserByID(userID, false)
if status != 200 || err != nil {
user, err := app.jf.UserByID(userID, false)
if err != nil {
app.err.Printf(lm.FailedGetUser, userID, lm.Jellyfin, err)
continue
}
@ -835,9 +831,9 @@ func (app *appContext) AdminPasswordReset(gc *gin.Context) {
// @tags Users
func (app *appContext) GetUsers(gc *gin.Context) {
var resp getUsersDTO
users, status, err := app.jf.GetUsers(false)
users, err := app.jf.GetUsers(false)
resp.UserList = make([]respUser, len(users))
if !(status == 200 || status == 204) || err != nil {
if err != nil {
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
respond(500, "Couldn't get users", gc)
return
@ -910,8 +906,8 @@ func (app *appContext) GetUsers(gc *gin.Context) {
func (app *appContext) SetAccountsAdmin(gc *gin.Context) {
var req setAccountsAdminDTO
gc.BindJSON(&req)
users, status, err := app.jf.GetUsers(false)
if !(status == 200 || status == 204) || err != nil {
users, err := app.jf.GetUsers(false)
if err != nil {
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
respond(500, "Couldn't get users", gc)
return
@ -942,8 +938,8 @@ func (app *appContext) SetAccountsAdmin(gc *gin.Context) {
func (app *appContext) ModifyLabels(gc *gin.Context) {
var req modifyEmailsDTO
gc.BindJSON(&req)
users, status, err := app.jf.GetUsers(false)
if !(status == 200 || status == 204) || err != nil {
users, err := app.jf.GetUsers(false)
if err != nil {
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
respond(500, "Couldn't get users", gc)
return
@ -976,11 +972,11 @@ func (app *appContext) modifyEmail(jfID string, addr string) {
emailStore.Addr = addr
app.storage.SetEmailsKey(jfID, emailStore)
if app.config.Section("ombi").Key("enabled").MustBool(false) {
ombiUser, code, err := app.getOmbiUser(jfID)
if code == 200 && err == nil {
ombiUser, err := app.getOmbiUser(jfID)
if err == nil {
ombiUser["emailAddress"] = addr
code, err = app.ombi.ModifyUser(ombiUser)
if code != 200 || err != nil {
err = app.ombi.ModifyUser(ombiUser)
if err != nil {
app.err.Printf(lm.FailedSetEmailAddress, lm.Ombi, jfID, err)
}
}
@ -1012,8 +1008,8 @@ func (app *appContext) modifyEmail(jfID string, addr string) {
func (app *appContext) ModifyEmails(gc *gin.Context) {
var req modifyEmailsDTO
gc.BindJSON(&req)
users, status, err := app.jf.GetUsers(false)
if !(status == 200 || status == 204) || err != nil {
users, err := app.jf.GetUsers(false)
if err != nil {
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
respond(500, "Couldn't get users", gc)
return
@ -1099,8 +1095,8 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
} else if req.From == "user" {
applyingFromType = lm.User
app.jf.CacheExpiry = time.Now()
user, status, err := app.jf.UserByID(req.ID, false)
if !(status == 200 || status == 204) || err != nil {
user, err := app.jf.UserByID(req.ID, false)
if err != nil {
app.err.Printf(lm.FailedGetUser, req.ID, lm.Jellyfin, err)
respond(500, "Couldn't get user", gc)
return
@ -1110,8 +1106,8 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
policy = user.Policy
}
if req.Homescreen {
displayprefs, status, err = app.jf.GetDisplayPreferences(req.ID)
if !(status == 200 || status == 204) || err != nil {
displayprefs, err = app.jf.GetDisplayPreferences(req.ID)
if err != nil {
app.err.Printf(lm.FailedGetJellyfinDisplayPrefs, req.ID, err)
respond(500, "Couldn't get displayprefs", gc)
return
@ -1136,26 +1132,25 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
app.debug.Printf(lm.DelayingRequests, requestDelayThreshold)
}
for _, id := range req.ApplyTo {
var status int
var err error
if req.Policy {
status, err = app.jf.SetPolicy(id, policy)
if !(status == 200 || status == 204) || err != nil {
errors["policy"][id] = fmt.Sprintf("%d: %s", status, err)
err = app.jf.SetPolicy(id, policy)
if err != nil {
errors["policy"][id] = err.Error()
}
}
if shouldDelay {
time.Sleep(250 * time.Millisecond)
}
if req.Homescreen {
status, err = app.jf.SetConfiguration(id, configuration)
err = app.jf.SetConfiguration(id, configuration)
errorString := ""
if !(status == 200 || status == 204) || err != nil {
errorString += fmt.Sprintf("Configuration %d: %v ", status, err)
if err != nil {
errorString += fmt.Sprintf("Configuration: %v", err)
} else {
status, err = app.jf.SetDisplayPreferences(id, displayprefs)
if !(status == 200 || status == 204) || err != nil {
errorString += fmt.Sprintf("Displayprefs %d: %v ", status, err)
err = app.jf.SetDisplayPreferences(id, displayprefs)
if err != nil {
errorString += fmt.Sprintf("Displayprefs;) %v ", err)
}
}
if errorString != "" {
@ -1164,18 +1159,18 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
}
if ombi != nil {
errorString := ""
user, status, err := app.getOmbiUser(id)
if status != 200 || err != nil {
errorString += fmt.Sprintf("Ombi GetUser %d: %v ", status, err)
user, err := app.getOmbiUser(id)
if err != nil {
errorString += fmt.Sprintf("Ombi GetUser: %v ", err)
} else {
// newUser := ombi
// newUser["id"] = user["id"]
// newUser["userName"] = user["userName"]
// newUser["alias"] = user["alias"]
// newUser["emailAddress"] = user["emailAddress"]
status, err = app.ombi.applyProfile(user, ombi)
if status != 200 || err != nil {
errorString += fmt.Sprintf("Apply %d: %v ", status, err)
err = app.ombi.applyProfile(user, ombi)
if err != nil {
errorString += fmt.Sprintf("Apply: %v ", err)
}
}
if errorString != "" {

31
api.go
View File

@ -148,18 +148,18 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
userID = reset.ID
username = reset.Username
status, err := app.jf.ResetPasswordAdmin(userID)
if !(status == 200 || status == 204) || err != nil {
err := app.jf.ResetPasswordAdmin(userID)
if err != nil {
app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, userID, err)
respondBool(status, false, gc)
respondBool(500, false, gc)
return
}
delete(app.internalPWRs, req.PIN)
} else {
resp, status, err := app.jf.ResetPassword(req.PIN)
if status != 200 || err != nil || !resp.Success {
resp, err := app.jf.ResetPassword(req.PIN)
if err != nil || !resp.Success {
app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, userID, err)
respondBool(status, false, gc)
respondBool(500, false, gc)
return
}
if req.Password == "" || len(resp.UsersReset) == 0 {
@ -170,14 +170,13 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
}
var user mediabrowser.User
var status int
var err error
if isInternal {
user, status, err = app.jf.UserByID(userID, false)
user, err = app.jf.UserByID(userID, false)
} else {
user, status, err = app.jf.UserByName(username, false)
user, err = app.jf.UserByName(username, false)
}
if status != 200 || err != nil {
if err != nil {
app.err.Printf(lm.FailedGetUser, userID, lm.Jellyfin, err)
respondBool(500, false, gc)
return
@ -195,8 +194,8 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
if isInternal {
prevPassword = ""
}
status, err = app.jf.SetPassword(user.ID, prevPassword, req.Password)
if !(status == 200 || status == 204) || err != nil {
err = app.jf.SetPassword(user.ID, prevPassword, req.Password)
if err != nil {
app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, user.ID, err)
respondBool(500, false, gc)
return
@ -210,15 +209,15 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
respondBool(200, true, gc)
return
} */
ombiUser, status, err := app.getOmbiUser(user.ID)
if status != 200 || err != nil {
ombiUser, err := app.getOmbiUser(user.ID)
if err != nil {
app.err.Printf(lm.FailedGetUser, user.ID, lm.Ombi, err)
respondBool(200, true, gc)
return
}
ombiUser["password"] = req.Password
status, err = app.ombi.ModifyUser(ombiUser)
if status != 200 || err != nil {
err = app.ombi.ModifyUser(ombiUser)
if err != nil {
app.err.Printf(lm.FailedChangePassword, lm.Ombi, user.ID, err)
respondBool(200, true, gc)
return

11
auth.go
View File

@ -2,6 +2,7 @@ package main
import (
"encoding/base64"
"errors"
"fmt"
"os"
"strings"
@ -166,19 +167,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) {
ok = false
user, status, err := app.authJf.Authenticate(username, password)
if status != 200 || err != nil {
if status == 401 || status == 400 {
user, err := app.authJf.Authenticate(username, password)
if err != nil {
if errors.Is(err, mediabrowser.ErrUnauthorized{}) {
app.logIpInfo(gc, userpage, fmt.Sprintf(lm.FailedAuthRequest, lm.InvalidUserOrPass))
respond(401, "Unauthorized", gc)
return
}
if status == 403 {
if errors.Is(err, mediabrowser.ErrForbidden{}) {
app.logIpInfo(gc, userpage, fmt.Sprintf(lm.FailedAuthRequest, lm.UserDisabled))
respond(403, "yourAccountWasDisabled", gc)
return
}
app.authLog(fmt.Sprintf(lm.FailedAuthJellyfin, app.jf.Server, status, err))
app.authLog(fmt.Sprintf(lm.FailedAuthJellyfin, app.jf.Server, err))
respond(500, "Jellyfin error", gc)
return
}

View File

@ -1,8 +1,11 @@
package common
import (
"errors"
"fmt"
"log"
lm "github.com/hrfee/jfa-go/logmessages"
)
// TimeoutHandler recovers from an http timeout or panic.
@ -12,7 +15,7 @@ type TimeoutHandler func()
func NewTimeoutHandler(name, addr string, noFail bool) TimeoutHandler {
return func() {
if r := recover(); r != nil {
out := fmt.Sprintf("Failed to authenticate with %s @ \"%s\": Timed out", name, addr)
out := fmt.Sprintf(lm.FailedAuth, name, addr, 0, lm.TimedOut)
if noFail {
log.Print(out)
} else {
@ -21,3 +24,50 @@ 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}
}
}

View File

@ -1,3 +1,7 @@
module github.com/hrfee/jfa-go/common
go 1.15
replace github.com/hrfee/jfa-go/logmessages => ../logmessages
go 1.22.4
require github.com/hrfee/jfa-go/logmessages v0.0.0-20240805130902-86c37fb4237b

View File

@ -62,18 +62,7 @@ html:not(.dark) .card.\@low:not(.\~neutral):not(.\~positive):not(.\~urge):not(.\
display: initial;
}
.page-container {
margin: 5% 20% 5% 20%;
}
@media (max-width: 1100px) {
.page-container {
margin: 2%;
margin-top: 5rem;
}
}
@media screen and (max-width: 1000px) {
@media screen and (max-width: 1024px) {
:root {
font-size: 0.9rem;
}

View File

@ -968,11 +968,10 @@ 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.
func (app *appContext) ReverseUserSearch(address string, matchUsername, matchEmail, matchContactMethod bool) (user mediabrowser.User, ok bool) {
ok = false
var status int
var err error = nil
if matchUsername {
user, status, err = app.jf.UserByName(address, false)
if status == 200 && err == nil {
user, err = app.jf.UserByName(address, false)
if err == nil {
ok = true
return
}
@ -983,8 +982,8 @@ func (app *appContext) ReverseUserSearch(address string, matchUsername, matchEma
err = app.storage.db.Find(&emailAddresses, badgerhold.Where("Addr").Eq(address))
if err == nil && len(emailAddresses) > 0 {
for _, emailUser := range emailAddresses {
user, status, err = app.jf.UserByID(emailUser.JellyfinID, false)
if status == 200 && err == nil {
user, err = app.jf.UserByID(emailUser.JellyfinID, false)
if err == nil {
ok = true
return
}
@ -997,8 +996,8 @@ func (app *appContext) ReverseUserSearch(address string, matchUsername, matchEma
if matchContactMethod {
for _, dcUser := range app.storage.GetDiscord() {
if RenderDiscordUsername(dcUser) == strings.ToLower(address) {
user, status, err = app.jf.UserByID(dcUser.JellyfinID, false)
if status == 200 && err == nil {
user, err = app.jf.UserByID(dcUser.JellyfinID, false)
if err == nil {
ok = true
return
}
@ -1009,8 +1008,8 @@ func (app *appContext) ReverseUserSearch(address string, matchUsername, matchEma
err = app.storage.db.Find(&telegramUsers, badgerhold.Where("Username").Eq(tgUsername))
if err == nil && len(telegramUsers) > 0 {
for _, telegramUser := range telegramUsers {
user, status, err = app.jf.UserByID(telegramUser.JellyfinID, false)
if status == 200 && err == nil {
user, err = app.jf.UserByID(telegramUser.JellyfinID, false)
if err == nil {
ok = true
return
}
@ -1020,8 +1019,8 @@ func (app *appContext) ReverseUserSearch(address string, matchUsername, matchEma
err = app.storage.db.Find(&matrixUsers, badgerhold.Where("UserID").Eq(address))
if err == nil && len(matrixUsers) > 0 {
for _, matrixUser := range matrixUsers {
user, status, err = app.jf.UserByID(matrixUser.JellyfinID, false)
if status == 200 && err == nil {
user, err = app.jf.UserByID(matrixUser.JellyfinID, false)
if err == nil {
ok = true
return
}

10
go.mod
View File

@ -33,13 +33,15 @@ require (
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a
github.com/hrfee/jfa-go/common v0.0.0-20240728190513-dabef831d769
github.com/hrfee/jfa-go/common v0.0.0-20240805130902-86c37fb4237b
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/jellyseerr v0.0.0-20240805130902-86c37fb4237b
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/ombi v0.0.0-20230626224816-f72960635dc3
github.com/hrfee/mediabrowser v0.3.14
github.com/hrfee/jfa-go/logmessages v0.0.0-20240805130902-86c37fb4237b
github.com/hrfee/jfa-go/ombi v0.0.0-20240805130902-86c37fb4237b
github.com/hrfee/mediabrowser v0.3.18
github.com/itchyny/timefmt-go v0.1.5
github.com/lithammer/shortuuid/v3 v3.0.7
github.com/mailgun/mailgun-go/v4 v4.9.1
@ -92,8 +94,6 @@ require (
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/mux v1.8.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/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.16.6 // indirect

16
go.sum
View File

@ -1,6 +1,7 @@
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/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/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
@ -94,6 +95,7 @@ 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/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/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
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/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
@ -140,6 +142,7 @@ 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-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/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.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@ -213,6 +216,7 @@ 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.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/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=
@ -224,10 +228,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/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hrfee/mediabrowser v0.3.13 h1:NgQNbq+JWwsP68BdWXL/rwbpfE/oO5LJ5KVkE+aNbX8=
github.com/hrfee/mediabrowser v0.3.13/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
github.com/hrfee/mediabrowser v0.3.14 h1:/hxtwefV4pA63fyHussk00JBFry6p+uDDOumA49M1eY=
github.com/hrfee/mediabrowser v0.3.14/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
github.com/hrfee/mediabrowser v0.3.17 h1:gerX/sxQ8V64mZ13I0zuK9hpCJnScdTpAzD2j0a7bvE=
github.com/hrfee/mediabrowser v0.3.17/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
github.com/hrfee/mediabrowser v0.3.18 h1:6UDyae0srEVjiMG+utPQfJJp4UId6/T3WN9EDCQKRTk=
github.com/hrfee/mediabrowser v0.3.18/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
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/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
@ -289,6 +293,7 @@ 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/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
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/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -351,6 +356,7 @@ 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.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
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 v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
@ -420,6 +426,7 @@ 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/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/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
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/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
@ -455,6 +462,7 @@ 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.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/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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=

View File

@ -16,7 +16,7 @@ func (app *appContext) clearEmails() {
app.debug.Println(lm.HousekeepingEmail)
emails := app.storage.GetEmails()
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
switch err.(type) {
case mediabrowser.ErrUserNotFound:
@ -32,7 +32,7 @@ func (app *appContext) clearDiscord() {
app.debug.Println(lm.HousekeepingDiscord)
discordUsers := app.storage.GetDiscord()
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
switch err.(type) {
case mediabrowser.ErrUserNotFound:
@ -53,7 +53,7 @@ func (app *appContext) clearMatrix() {
app.debug.Println(lm.HousekeepingMatrix)
matrixUsers := app.storage.GetMatrix()
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
switch err.(type) {
case mediabrowser.ErrUserNotFound:
@ -69,7 +69,7 @@ func (app *appContext) clearTelegram() {
app.debug.Println(lm.HousekeepingTelegram)
telegramUsers := app.storage.GetTelegram()
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
switch err.(type) {
case mediabrowser.ErrUserNotFound:

View File

@ -6,7 +6,7 @@
<title>404 - jfa-go</title>
</head>
<body class="section">
<div class="page-container">
<div class="page-container m-2 lg:my-20 lg:mx-64">
<div class="card">
<h1 class="heading">Page not found.</h1>
<p class="content">

View File

@ -44,7 +44,7 @@
</form>
</div>
<div id="modal-about" class="modal">
<div class="relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 content card">
<div class="relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/2 content card">
<img src="{{ .urlBase }}/banner.svg" class="banner header" alt="jfa-go banner">
<span class="heading"><span class="modal-close">&times;</span></span>
<p>{{ .strings.version }} <span class="text-black dark:text-white font-mono bg-inherit">{{ .version }}</span></p>
@ -535,58 +535,56 @@
</form>
</div>
<div id="notification-box"></div>
<div class="top-4 left-4 absolute flex flex-row gap-2">
<span class="dropdown z-[11]" tabindex="0" id="lang-dropdown">
<span class="button ~urge dropdown-button">
<i class="ri-global-line"></i>
<span class="ml-2 chev"></span>
</span>
<div class="dropdown-display">
<div class="card ~neutral @low">
<label class="switch pb-4">
<input type="radio" name="lang-time" id="lang-12h">
<span>{{ .strings.time12h }}</span>
</label>
<label class="switch pb-4">
<input type="radio" name="lang-time" id="lang-24h">
<span>{{ .strings.time24h }}</span>
</label>
<div id="lang-list"></div>
</div>
<div class="page-container m-2 lg:my-20 lg:mx-64 flex flex-col gap-4">
<div class="top-2 inset-x-2 lg:absolute flex flex-row justify-between">
<div class="flex flex-row gap-2">
<span class="dropdown z-[11]" tabindex="0" id="lang-dropdown">
<span class="button ~urge dropdown-button">
<i class="ri-global-line"></i>
<span class="ml-2 chev"></span>
</span>
<div class="dropdown-display">
<div class="card ~neutral @low">
<label class="switch pb-4">
<input type="radio" name="lang-time" id="lang-12h">
<span>{{ .strings.time12h }}</span>
</label>
<label class="switch pb-4">
<input type="radio" name="lang-time" id="lang-24h">
<span>{{ .strings.time24h }}</span>
</label>
<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>
</span>
<span class="button ~warning" alt="{{ .strings.theme }}" id="button-theme"><i class="ri-sun-line"></i></span>
</div>
{{ if .userPageEnabled }}
<div class="top-4 right-4 absolute">
<a class="button ~info" href="{{ .urlBase }}/my/account"><i class="ri-account-circle-fill mr-2"></i>{{ .strings.myAccount }}</a>
</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 class="flex flex-row gap-2">
<span class="button ~critical @low unfocused" id="logout-button">{{ .strings.logout }}</span>
{{ if .userPageEnabled }}
<div class="">
<a class="button ~info" href="{{ .urlBase }}/my/account"><i class="ri-account-circle-fill mr-2"></i>{{ .strings.myAccount }}</a>
</div>
{{ end }}
</div>
</div>
<div id="tab-invites">
<div class="card @low invites dark:~d_neutral mb-4 overflow-visible">
<header>
<div class="flex flex-row overflow-x-scroll items-center gap-2">
<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>
<div id="invites" class="mt-2"></div>
<div id="invites"></div>
</div>
<div class="card @low dark:~d_neutral">
<div class="card @low dark:~d_neutral flex flex-col gap-2">
<span class="heading">{{ .strings.create }}</span>
<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 grow">
<div class="flex flex-col md:flex-row gap-3" id="create-inv">
<div class="card ~neutral @low flex flex-col gap-2 flex-1">
<div class="flex flex-row gap-2">
<label class="w-1/2">
<input type="radio" name="duration" class="unfocused" id="radio-inv-duration" checked>
@ -694,7 +692,7 @@
<input type="text" id="create-user-label" class="input ~neutral @low">
</div>
</div>
<div class="card ~neutral @low flex flex-col justify-between gap-2 grow">
<div class="card ~neutral @low flex flex-col justify-between gap-2 flex-1">
<div class="flex flex-col gap-2">
<div class="flex flex-col gap-4">
<label class="label supra" for="create-uses">{{ .strings.inviteNumberOfUses }}</label>
@ -738,7 +736,7 @@
</div>
</div>
</div>
<div id="tab-accounts" class="unfocused">
<div id="tab-accounts" class="flex flex-col gap-4 unfocused">
<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 class="flex flex-col md:flex-row align-middle gap-2">
@ -837,7 +835,7 @@
</div>
</div>
</div>
<div id="tab-activity" class="unfocused">
<div id="tab-activity" class="flex flex-col gap-4 unfocused">
<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 class="flex flex-col md:flex-row align-middle gap-2">
@ -894,8 +892,8 @@
</div>
</div>
</div>
<div id="tab-settings" class="unfocused">
<div class="card @low dark:~d_neutral settings overflow">
<div id="tab-settings" class="flex flex-col gap-4 unfocused">
<div class="card @low dark:~d_neutral settings overflow flex flex-col 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">
<span class="heading">{{ .strings.settings }}</span>
@ -912,16 +910,18 @@
</div>
</div>
<div class="flex flex-col md:flex-row gap-3">
<div class="card @low dark:~d_neutral col" id="settings-sidebar">
<div class="md:card @low dark:~d_neutral flex md:flex flex-col gap-2 flex-1" id="settings-sidebar">
<div class="flex flex-row justify-between">
<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 mb-2 settings-search-clear" aria-label="{{ .strings.clearSearch }}" text="{{ .strings.clearSearch }}"><i class="ri-close-line"></i></button>
<input type="search" class="field ~neutral @low input settings-section-button justify-between" 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>
</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>
<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 class="card ~neutral @low col overflow" id="settings-panel">
<div class="card ~neutral @low overflow flex-1" id="settings-panel">
<div class="settings-section unfocused h-[100%]" id="settings-not-found">
<div class="flex flex-col h-[100%] justify-center items-center">
<span class="text-2xl font-medium italic mb-2">{{ .strings.noResultsFound }}</span>

View File

@ -6,7 +6,7 @@
<title>Crash report</title>
</head>
<body>
<div class="page-container">
<div class="page-container m-2 lg:my-20 lg:mx-64">
<div class="card ~critical sectioned">
<section class="section ~critical">
<span class="heading">Crash report for jfa-go</span>

View File

@ -6,7 +6,7 @@
<title>{{ .strings.successHeader }} - jfa-go</title>
</head>
<body class="section">
<div class="page-container">
<div class="page-container m-2 lg:my-20 lg:mx-64">
<div class="card ~neutral @low mb-4">
<span class="heading mb-4">{{ .strings.successHeader }}</span>
<p class="content my-4">{{ .successMessage }}</p>

View File

@ -35,7 +35,7 @@
</div>
</div>
{{ template "account-linking.html" . }}
<div class="top-4 left-4 absolute">
<div class="top-2 left-2 absolute">
<span class="dropdown" tabindex="0" id="lang-dropdown">
<span class="button ~urge dropdown-button">
<i class="ri-global-line"></i>
@ -48,7 +48,7 @@
</span>
</div>
<div id="notification-box"></div>
<div class="page-container">
<div class="page-container m-2 lg:my-20 lg:mx-64">
<div class="card dark:~d_neutral @low">
<div class="flex flex-col md:flex-row gap-3 items-baseline mb-2">
<span class="heading mr-5">

View File

@ -6,7 +6,7 @@
<title>Invalid Code - jfa-go</title>
</head>
<body class="section">
<div class="page-container">
<div class="page-container m-2 lg:my-20 lg:mx-64">
<div class="card">
<h1 class="text-3xl font-semibold">Invalid invite code.</h1>
<p class="content">The code above was either incorrect, or has expired.</p>

View File

@ -11,7 +11,7 @@
<span id="copy-notification" class="unfocused">{{ .strings.copied }}</span>
</div>
{{ end }}
<div class="page-container">
<div class="page-container m-2 lg:my-20 lg:mx-64">
<div class="card ~neutral @low mb-4">
<span class="heading mb-4">
{{ if .success }}

View File

@ -7,7 +7,7 @@
</head>
<body class="max-w-full overflow-x-hidden section">
<div id="notification-box"></div>
<div class="top-4 left-4 absolute">
<div class="top-2 left-2 absolute">
<span class="dropdown" tabindex="0" id="lang-dropdown">
<span class="button ~urge dropdown-button">
<i class="ri-global-line"></i>
@ -19,7 +19,7 @@
</div>
</span>
</div>
<div class="page-container" id="page-container">
<div class="page-container m-2 lg:my-20 lg:mx-64" id="page-container">
<div class="card ~neutral @low mb-2">
<div class="row">
<img class="banner header" src="banner.svg" alt="jfa-go" />

View File

@ -79,7 +79,7 @@
{{ template "login-modal.html" . }}
{{ template "account-linking.html" . }}
<div id="notification-box"></div>
<div class="top-4 left-4 absolute">
<div class="top-2 left-2 absolute">
<span class="dropdown" tabindex="0" id="lang-dropdown">
<span class="button ~urge dropdown-button">
<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 ~critical @low mb-4 unfocused" id="logout-button">{{ .strings.logout }}</span>
</div>
<div class="top-4 right-4 absolute">
<div class="top-2 right-2 absolute">
<a class="button ~info unfocused" href="/" id="admin-back-button"><i class="ri-arrow-left-fill mr-2"></i>{{ .strings.admin }}</a>
</div>
<div class="page-container unfocused">
<div class="page-container m-2 lg:my-20 lg:mx-64 unfocused">
<div class="card @low dark:~d_neutral mb-4" id="card-user">
<span class="heading mb-2"></span>
</div>

View File

@ -58,8 +58,8 @@ func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
}
func (app *appContext) SynchronizeJellyseerrUsers() {
users, status, err := app.jf.GetUsers(false)
if err != nil || status != 200 {
users, err := app.jf.GetUsers(false)
if err != nil {
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
return
}

View File

@ -12,7 +12,7 @@ import (
"strings"
"time"
"github.com/hrfee/jfa-go/common"
co "github.com/hrfee/jfa-go/common"
)
const (
@ -28,13 +28,13 @@ type Jellyseerr struct {
userCache map[string]User // Map of jellyfin IDs to users
cacheExpiry time.Time
cacheLength time.Duration
timeoutHandler common.TimeoutHandler
timeoutHandler co.TimeoutHandler
LogRequestBodies bool
AutoImportUsers bool
}
// NewJellyseerr returns an Ombi object.
func NewJellyseerr(server, key string, timeoutHandler common.TimeoutHandler) *Jellyseerr {
func NewJellyseerr(server, key string, timeoutHandler co.TimeoutHandler) *Jellyseerr {
if !strings.HasSuffix(server, API_SUFFIX) {
server = server + API_SUFFIX
}
@ -82,25 +82,23 @@ func (js *Jellyseerr) req(mode string, uri string, data any, queryParams url.Val
}
}
resp, err := js.httpClient.Do(req)
reqFailed := err != nil || !(resp.StatusCode == 200 || resp.StatusCode == 201)
err = co.GenericErr(resp.StatusCode, err)
defer js.timeoutHandler()
var responseText string
defer resp.Body.Close()
if response || reqFailed {
if response || err != nil {
responseText, err = js.decodeResp(resp)
if err != nil {
return responseText, resp.StatusCode, err
}
}
if reqFailed {
if err != nil {
var msg ErrorDTO
err = json.Unmarshal([]byte(responseText), &msg)
if err != nil {
return responseText, resp.StatusCode, err
}
if msg.Message == "" {
err = fmt.Errorf("failed (error %d)", resp.StatusCode)
} else {
if msg.Message != "" {
err = fmt.Errorf("got %d: %s", resp.StatusCode, msg.Message)
}
return responseText, resp.StatusCode, err
@ -145,14 +143,11 @@ func (js *Jellyseerr) ImportFromJellyfin(jfIDs ...string) ([]User, error) {
params := map[string]interface{}{
"jellyfinUserIds": jfIDs,
}
resp, status, err := js.post(js.server+"/user/import-from-jellyfin", params, true)
resp, _, err := js.post(js.server+"/user/import-from-jellyfin", params, true)
var data []User
if err != nil {
return data, err
}
if status != 200 && status != 201 {
return data, fmt.Errorf("failed (error %d)", status)
}
err = json.Unmarshal([]byte(resp), &data)
for _, u := range data {
if u.JellyfinUserID != "" {
@ -197,15 +192,11 @@ func (js *Jellyseerr) getUserPage(page int) (GetUsersDTO, error) {
if js.LogRequestBodies {
fmt.Printf("Jellyseerr API Client: Sending with URL params \"%+v\"\n", params)
}
resp, status, err := js.get(js.server+"/user", nil, params)
resp, _, err := js.get(js.server+"/user", nil, params)
var data GetUsersDTO
if status != 200 {
return data, fmt.Errorf("failed (error %d)", status)
if err == nil {
err = json.Unmarshal([]byte(resp), &data)
}
if err != nil {
return data, err
}
err = json.Unmarshal([]byte(resp), &data)
return data, err
}
@ -261,12 +252,9 @@ func (js *Jellyseerr) getUser(jfID string) (User, error) {
}
func (js *Jellyseerr) Me() (User, error) {
resp, status, err := js.get(js.server+"/auth/me", nil, url.Values{})
resp, _, err := js.get(js.server+"/auth/me", nil, url.Values{})
var data User
data.ID = -1
if status != 200 {
return data, fmt.Errorf("failed (error %d)", status)
}
if err != nil {
return data, err
}
@ -281,13 +269,10 @@ func (js *Jellyseerr) GetPermissions(jfID string) (Permissions, error) {
return data.Permissions, err
}
resp, status, err := js.get(fmt.Sprintf(js.server+"/user/%d/settings/permissions", u.ID), nil, url.Values{})
resp, _, err := js.get(fmt.Sprintf(js.server+"/user/%d/settings/permissions", u.ID), nil, url.Values{})
if err != nil {
return data.Permissions, err
}
if status != 200 {
return data.Permissions, fmt.Errorf("failed (error %d)", status)
}
err = json.Unmarshal([]byte(resp), &data)
return data.Permissions, err
}
@ -298,13 +283,10 @@ func (js *Jellyseerr) SetPermissions(jfID string, perm Permissions) error {
return err
}
_, status, err := js.post(fmt.Sprintf(js.server+"/user/%d/settings/permissions", u.ID), permissionsDTO{Permissions: perm}, false)
_, _, err = js.post(fmt.Sprintf(js.server+"/user/%d/settings/permissions", u.ID), permissionsDTO{Permissions: perm}, false)
if err != nil {
return err
}
if status != 200 && status != 201 {
return fmt.Errorf("failed (error %d)", status)
}
u.Permissions = perm
js.userCache[jfID] = u
return nil
@ -316,13 +298,10 @@ func (js *Jellyseerr) ApplyTemplateToUser(jfID string, tmpl UserTemplate) error
return err
}
_, status, err := js.put(fmt.Sprintf(js.server+"/user/%d", u.ID), tmpl, false)
_, _, err = js.put(fmt.Sprintf(js.server+"/user/%d", u.ID), tmpl, false)
if err != nil {
return err
}
if status != 200 && status != 201 {
return fmt.Errorf("failed (error %d)", status)
}
u.UserTemplate = tmpl
js.userCache[jfID] = u
return nil
@ -337,13 +316,10 @@ func (js *Jellyseerr) ModifyUser(jfID string, conf map[UserField]any) error {
return err
}
_, status, err := js.put(fmt.Sprintf(js.server+"/user/%d", u.ID), conf, false)
_, _, err = js.put(fmt.Sprintf(js.server+"/user/%d", u.ID), conf, false)
if err != nil {
return err
}
if status != 200 && status != 201 {
return fmt.Errorf("failed (error %d)", status)
}
// Lazily just invalidate the cache.
js.cacheExpiry = time.Now()
return nil
@ -355,10 +331,7 @@ func (js *Jellyseerr) DeleteUser(jfID string) error {
return err
}
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)
}
_, err = js.delete(fmt.Sprintf(js.server+"/user/%d", u.ID), nil)
if err != nil {
return err
}
@ -376,13 +349,10 @@ func (js *Jellyseerr) GetNotificationPreferences(jfID string) (Notifications, er
func (js *Jellyseerr) GetNotificationPreferencesByID(jellyseerrID int64) (Notifications, error) {
var data Notifications
resp, status, err := js.get(fmt.Sprintf(js.server+"/user/%d/settings/notifications", jellyseerrID), nil, url.Values{})
resp, _, err := js.get(fmt.Sprintf(js.server+"/user/%d/settings/notifications", jellyseerrID), nil, url.Values{})
if err != nil {
return data, err
}
if status != 200 {
return data, fmt.Errorf("failed (error %d)", status)
}
err = json.Unmarshal([]byte(resp), &data)
return data, err
}
@ -397,13 +367,10 @@ func (js *Jellyseerr) ApplyNotificationsTemplateToUser(jfID string, tmpl Notific
return err
}
_, status, err := js.post(fmt.Sprintf(js.server+"/user/%d/settings/notifications", u.ID), tmpl, false)
_, _, err = js.post(fmt.Sprintf(js.server+"/user/%d/settings/notifications", u.ID), tmpl, false)
if err != nil {
return err
}
if status != 200 && status != 201 {
return fmt.Errorf("failed (error %d)", status)
}
return nil
}
@ -413,13 +380,10 @@ func (js *Jellyseerr) ModifyNotifications(jfID string, conf map[NotificationsFie
return err
}
_, status, err := js.post(fmt.Sprintf(js.server+"/user/%d/settings/notifications", u.ID), conf, false)
_, _, err = js.post(fmt.Sprintf(js.server+"/user/%d/settings/notifications", u.ID), conf, false)
if err != nil {
return err
}
if status != 200 && status != 201 {
return fmt.Errorf("failed (error %d)", status)
}
return nil
}
@ -429,11 +393,8 @@ func (js *Jellyseerr) GetUsers() (map[string]User, error) {
}
func (js *Jellyseerr) UserByID(jellyseerrID int64) (User, error) {
resp, status, err := js.get(js.server+fmt.Sprintf("/user/%d", jellyseerrID), nil, url.Values{})
resp, _, err := js.get(js.server+fmt.Sprintf("/user/%d", jellyseerrID), nil, url.Values{})
var data User
if status != 200 {
return data, fmt.Errorf("failed (error %d)", status)
}
if err != nil {
return data, err
}
@ -447,13 +408,10 @@ func (js *Jellyseerr) ModifyMainUserSettings(jfID string, conf MainUserSettings)
return err
}
_, status, err := js.post(fmt.Sprintf(js.server+"/user/%d/settings/main", u.ID), conf, false)
_, _, err = js.post(fmt.Sprintf(js.server+"/user/%d/settings/main", u.ID), conf, false)
if err != nil {
return err
}
if status != 200 && status != 201 {
return fmt.Errorf("failed (error %d)", status)
}
// Lazily just invalidate the cache.
js.cacheExpiry = time.Now()
return nil

View File

@ -1,3 +1,3 @@
module github.com/hrfee/logmessages
module github.com/hrfee/jfa-go/logmessages
go 1.22.4

View File

@ -45,7 +45,7 @@ const (
UsingTLS = "Using TLS/HTTP2"
UsingOmbi = "Starting " + " + Ombi + " + " client"
UsingOmbi = "Starting " + Ombi + " client"
UsingJellyseerr = "Starting " + Jellyseerr + " client"
UsingEmby = "Using Emby server type (EXPERIMENTAL: PWRs are not available, and support is limited.)"
UsingJellyfin = "Using " + Jellyfin + " server type"
@ -54,6 +54,13 @@ const (
AuthJellyfin = "Authenticated with " + Jellyfin + " @ \"%s\""
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"
FailedInitDiscord = "Failed to initialize Discord daemon: %v"
@ -213,7 +220,6 @@ const (
FailedGetDiscordChannel = "Failed to get " + Discord + " channel \"%s\": %v"
MonitorAllDiscordChannels = "Will monitor all " + Discord + " channels"
FailedCreateDiscordDMChannel = "Failed to create " + Discord + " private DM channel with \"%s\": %v"
NotFound = "not found"
RegisterDiscordChoice = "Registered " + Discord + " %s choice \"%s\""
FailedRegisterDiscordChoices = "Failed to register " + Discord + " %s choices: %v"
FailedDeregDiscordChoice = "Failed to deregister " + Discord + " %s choice \"%s\": %v"

12
main.go
View File

@ -154,8 +154,8 @@ func test(app *appContext) {
for n, v := range settings {
fmt.Println(n, ":", v)
}
users, status, err := app.jf.GetUsers(false)
fmt.Printf("GetUsers: code %d err %s maplength %d\n", status, err, len(users))
users, err := app.jf.GetUsers(false)
fmt.Printf("GetUsers: err %s maplength %d\n", err, len(users))
fmt.Printf("View output? [y/n]: ")
var choice string
fmt.Scanln(&choice)
@ -166,8 +166,8 @@ func test(app *appContext) {
fmt.Printf("Enter a user to grab: ")
var username string
fmt.Scanln(&username)
user, status, err := app.jf.UserByName(username, false)
fmt.Printf("UserByName (%s): code %d err %s", username, status, err)
user, err := app.jf.UserByName(username, false)
fmt.Printf("UserByName (%s): code %d err %s", username, err)
out, _ := json.MarshalIndent(user, "", " ")
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,
LogFailures: true,
}
_, status, err = app.jf.MustAuthenticate(app.config.Section("jellyfin").Key("username").String(), app.config.Section("jellyfin").Key("password").String(), retryOpts)
if status != 200 || err != nil {
_, err = app.jf.MustAuthenticate(app.config.Section("jellyfin").Key("username").String(), app.config.Section("jellyfin").Key("password").String(), retryOpts)
if err != nil {
app.err.Fatalf(lm.FailedAuthJellyfin, server, status, err)
}
app.info.Printf(lm.AuthJellyfin, server)

View File

@ -185,14 +185,14 @@ func linkExistingOmbiDiscordTelegram(app *appContext) error {
idList[user.JellyfinID] = vals
}
for jfID, ids := range idList {
ombiUser, status, err := app.getOmbiUser(jfID)
if status != 200 || err != nil {
app.debug.Printf("Failed to get Ombi user with Discord/Telegram \"%s\"/\"%s\" (%d): %v", ids[0], ids[1], status, err)
ombiUser, err := app.getOmbiUser(jfID)
if err != nil {
app.debug.Printf("Failed to get Ombi user with Discord/Telegram \"%s\"/\"%s\": %v", ids[0], ids[1], err)
continue
}
_, status, err = app.ombi.SetNotificationPrefs(ombiUser, ids[0], ids[1])
if status != 200 || err != nil {
app.debug.Printf("Failed to set prefs for Ombi user \"%s\" (%d): %v", ombiUser["userName"].(string), status, err)
_, err = app.ombi.SetNotificationPrefs(ombiUser, ids[0], ids[1])
if err != nil {
app.debug.Printf("Failed to set prefs for Ombi user \"%s\": %v", ombiUser["userName"].(string), err)
continue
}
}

View File

@ -3,3 +3,5 @@ module github.com/hrfee/jfa-go/ombi
replace github.com/hrfee/jfa-go/common => ../common
go 1.15
require github.com/hrfee/jfa-go/common v0.0.0-20240805130902-86c37fb4237b // indirect

View File

@ -10,7 +10,7 @@ import (
"strings"
"time"
"github.com/hrfee/jfa-go/common"
co "github.com/hrfee/jfa-go/common"
)
const (
@ -26,11 +26,11 @@ type Ombi struct {
userCache []map[string]interface{}
cacheExpiry time.Time
cacheLength int
timeoutHandler common.TimeoutHandler
timeoutHandler co.TimeoutHandler
}
// NewOmbi returns an Ombi object.
func NewOmbi(server, key string, timeoutHandler common.TimeoutHandler) *Ombi {
func NewOmbi(server, key string, timeoutHandler co.TimeoutHandler) *Ombi {
return &Ombi{
server: server,
key: key,
@ -133,17 +133,19 @@ func (ombi *Ombi) put(url string, data map[string]interface{}, response bool) (s
}
// ModifyUser applies the given modified user object to the corresponding user.
func (ombi *Ombi) ModifyUser(user map[string]interface{}) (status int, err error) {
func (ombi *Ombi) ModifyUser(user map[string]interface{}) (err error) {
if _, ok := user["id"]; !ok {
err = fmt.Errorf("No ID provided")
return
}
var status int
_, status, err = ombi.put(ombi.server+"/api/v1/Identity/", user, false)
err = co.GenericErr(status, err)
return
}
// DeleteUser deletes the user corresponding to the given ID.
func (ombi *Ombi) DeleteUser(id string) (code int, err error) {
func (ombi *Ombi) DeleteUser(id string) (err error) {
url := fmt.Sprintf("%s/api/v1/Identity/%s", ombi.server, id)
req, _ := http.NewRequest("DELETE", url, nil)
req.Header.Add("Content-Type", "application/json")
@ -152,18 +154,19 @@ func (ombi *Ombi) DeleteUser(id string) (code int, err error) {
}
resp, err := ombi.httpClient.Do(req)
defer ombi.timeoutHandler()
return resp.StatusCode, err
return co.GenericErr(resp.StatusCode, err)
}
// UserByID returns the user corresponding to the provided ID.
func (ombi *Ombi) UserByID(id string) (result map[string]interface{}, code int, err error) {
func (ombi *Ombi) UserByID(id string) (result map[string]interface{}, err error) {
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)
return
}
// GetUsers returns all users on the Ombi instance.
func (ombi *Ombi) GetUsers() ([]map[string]interface{}, int, error) {
func (ombi *Ombi) GetUsers() ([]map[string]interface{}, error) {
if time.Now().After(ombi.cacheExpiry) {
resp, code, err := ombi.getJSON(fmt.Sprintf("%s/api/v1/Identity/Users", ombi.server), nil)
var result []map[string]interface{}
@ -172,9 +175,10 @@ func (ombi *Ombi) GetUsers() ([]map[string]interface{}, int, error) {
if (code == 200 || code == 204) && err == nil {
ombi.cacheExpiry = time.Now().Add(time.Minute * time.Duration(ombi.cacheLength))
}
return result, code, err
err = co.GenericErr(code, err)
return result, err
}
return ombi.userCache, 200, nil
return ombi.userCache, nil
}
// Strip these from a user when saving as a template.
@ -190,9 +194,9 @@ var stripFromOmbi = []string{
}
// TemplateByID returns a template based on the user corresponding to the provided ID's settings.
func (ombi *Ombi) TemplateByID(id string) (result map[string]interface{}, code int, err error) {
result, code, err = ombi.UserByID(id)
if err != nil || code != 200 {
func (ombi *Ombi) TemplateByID(id string) (result map[string]interface{}, err error) {
result, err = ombi.UserByID(id)
if err != nil {
return
}
for _, key := range stripFromOmbi {
@ -209,24 +213,25 @@ func (ombi *Ombi) TemplateByID(id string) (result map[string]interface{}, code i
}
// NewUser creates a new user with the given username, password and email address.
func (ombi *Ombi) NewUser(username, password, email string, template map[string]interface{}) ([]string, int, error) {
func (ombi *Ombi) NewUser(username, password, email string, template map[string]interface{}) ([]string, error) {
url := fmt.Sprintf("%s/api/v1/Identity", ombi.server)
user := template
user["userName"] = username
user["password"] = password
user["emailAddress"] = email
resp, code, err := ombi.post(url, user, true)
err = co.GenericErr(code, err)
var data map[string]interface{}
json.Unmarshal([]byte(resp), &data)
if err != nil || code != 200 {
if err != nil {
var lst []string
if data["errors"] != nil {
lst = data["errors"].([]string)
}
return lst, code, err
return lst, err
}
ombi.cacheExpiry = time.Now()
return nil, code, err
return nil, err
}
type NotificationPref struct {
@ -236,7 +241,7 @@ type NotificationPref struct {
Enabled bool `json:"enabled"`
}
func (ombi *Ombi) SetNotificationPrefs(user map[string]interface{}, discordID, telegramUser string) (result string, code int, err error) {
func (ombi *Ombi) SetNotificationPrefs(user map[string]interface{}, discordID, telegramUser string) (result string, err error) {
id := user["id"].(string)
url := fmt.Sprintf("%s/api/v1/Identity/NotificationPreferences", ombi.server)
data := []NotificationPref{}
@ -246,6 +251,8 @@ func (ombi *Ombi) SetNotificationPrefs(user map[string]interface{}, discordID, t
if telegramUser != "" {
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)})
err = co.GenericErr(code, err)
return
}

View File

@ -14,8 +14,8 @@ import (
// 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) {
pin := genAuthToken()
user, status, err := app.jf.UserByID(userID, false)
if err != nil || status != 200 {
user, err := app.jf.UserByID(userID, false)
if err != nil {
return InternalPWR{}, err
}
pwr := InternalPWR{
@ -95,8 +95,8 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) {
}
app.info.Printf("New password reset for user \"%s\"", pwr.Username)
if currentTime := time.Now(); pwr.Expiry.After(currentTime) {
user, status, err := app.jf.UserByName(pwr.Username, false)
if !(status == 200 || status == 204) || err != nil || user.ID == "" {
user, err := app.jf.UserByName(pwr.Username, false)
if err != nil || user.ID == "" {
app.err.Printf(lm.FailedGetUser, pwr.Username, lm.Jellyfin, err)
return
}

View File

@ -91,19 +91,22 @@ func (app *appContext) TestJF(gc *gin.Context) {
tempjf.SetTransport(transport)
}
user, status, err := tempjf.Authenticate(req.Username, req.Password)
if !(status == 200 || status == 204) || err != nil {
user, err := tempjf.Authenticate(req.Username, req.Password)
if err != nil {
msg := ""
switch status {
case 0:
msg = "errorConnectionRefused"
status = 500
case 401:
status := 500
switch err.(type) {
case mediabrowser.ErrUnauthorized:
msg = "errorInvalidUserPass"
case 403:
status = 401
case mediabrowser.ErrForbidden:
msg = "errorUserDisabled"
case 404:
status = 403
case mediabrowser.ErrNotFound:
msg = "error404"
status = 404
default:
msg = "errorConnectionRefused"
}
app.err.Printf(lm.FailedAuthJellyfin, req.Server, status, err)
if msg != "" {

View File

@ -52,7 +52,7 @@ sudo apt-get install jfa-go-tray
</div>
</div>
</div>
<div class="page-container" id="page-container">
<div class="page-container m-2 lg:my-20 lg:mx-72" id="page-container">
<div class="card ~neutral @low mb-1">
<div class="row col flex center">
<span class="heading welcome">jellyfin-accounts (go)</span>

View File

@ -627,6 +627,8 @@ export class settingsList {
private _saveNoRestart = document.getElementById("settings-apply-no-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 _sidebar = document.getElementById("settings-sidebar") as HTMLDivElement;
private _visibleSection: string;
@ -650,7 +652,7 @@ export class settingsList {
this._sections[name] = section;
this._panel.appendChild(this._sections[name].asElement());
const button = document.createElement("span") as HTMLSpanElement;
button.classList.add("button", "~neutral", "@low", "settings-section-button", "justify-between", "mb-2");
button.classList.add("button", "~neutral", "@low", "settings-section-button", "justify-between");
button.textContent = s.meta.name;
if (subButton) { button.appendChild(subButton); }
button.onclick = () => { this._showPanel(name); };
@ -905,63 +907,73 @@ export class settingsList {
window.modals.matrix.show();
}
reload = () => _get("/config", null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status != 200) {
window.notifications.customError("settingsLoadError", window.lang.notif("errorLoadSettings"));
return;
}
this._settings = req.response as Settings;
for (let name of this._settings.order) {
if (name in this._sections) {
this._sections[name].update(this._settings.sections[name]);
} else {
if (name == "messages" || name == "user_page") {
const editButton = document.createElement("div");
editButton.classList.add("tooltip", "left");
editButton.innerHTML = `
<span class="button ~neutral @low">
<i class="icon ri-edit-line"></i>
</span>
<span class="content sm">
${window.lang.get("strings", "customizeMessages")}
</span>
`;
(editButton.querySelector("span.button") as HTMLSpanElement).onclick = () => {
this._messageEditor.showList(name == "messages" ? "email" : "user");
};
this.addSection(name, this._settings.sections[name], editButton);
} else if (name == "updates") {
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);
reload = () => {
for (let i = 0; i < this._loader.children.length; i++) {
this._loader.children[i].classList.add("invisible");
}
addLoader(this._loader, false, true);
_get("/config", null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status != 200) {
window.notifications.customError("settingsLoadError", window.lang.notif("errorLoadSettings"));
return;
}
this._settings = req.response as Settings;
for (let name of this._settings.order) {
if (name in this._sections) {
this._sections[name].update(this._settings.sections[name]);
} else {
this.addSection(name, this._settings.sections[name]);
if (name == "messages" || name == "user_page") {
const editButton = document.createElement("div");
editButton.classList.add("tooltip", "left");
editButton.innerHTML = `
<span class="button ~neutral @low">
<i class="icon ri-edit-line"></i>
</span>
<span class="content sm">
${window.lang.get("strings", "customizeMessages")}
</span>
`;
(editButton.querySelector("span.button") as HTMLSpanElement).onclick = () => {
this._messageEditor.showList(name == "messages" ? "email" : "user");
};
this.addSection(name, this._settings.sections[name], editButton);
} else if (name == "updates") {
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]);
}
}
}
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.
search = (query: string) => {

View File

@ -24,8 +24,8 @@ func (app *appContext) checkUsers() {
}
app.info.Println(lm.CheckUserExpiries)
users, status, err := app.jf.GetUsers(false)
if err != nil || status != 200 {
users, err := app.jf.GetUsers(false)
if err != nil {
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
return
}
@ -89,7 +89,7 @@ func (app *appContext) checkUsers() {
err, _, _ = app.SetUserDisabled(user, true)
activity.Type = ActivityDisabled
}
if !(status == 200 || status == 204) || err != nil {
if err != nil {
app.err.Printf(lm.FailedDeleteOrDisableExpiredUser, user.ID, err)
continue
}

View File

@ -1,7 +1,6 @@
package main
import (
"fmt"
"time"
"github.com/gin-gonic/gin"
@ -68,7 +67,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 != "" {
out.Message = lm.UserExists
deferLogInfo(lm.FailedCreateUser, lm.Jellyfin, p.Req.Username, out.Message)
@ -76,10 +75,9 @@ func (app *appContext) NewUserPostVerification(p NewUserParams) (out NewUserData
return
}
var status int
var err error
out.User, status, err = app.jf.NewUser(p.Req.Username, p.Req.Password)
if !(status == 200 || status == 204) || err != nil {
out.User, err = app.jf.NewUser(p.Req.Username, p.Req.Password)
if err != nil {
out.Message = err.Error()
deferLogError(lm.FailedCreateUser, lm.Jellyfin, p.Req.Username, out.Message)
out.Status = 401
@ -100,15 +98,15 @@ func (app *appContext) NewUserPostVerification(p NewUserParams) (out NewUserData
}, p.ContextForIPLogging, (p.SourceType != ActivityAdmin))
if p.Profile != nil {
status, err = app.jf.SetPolicy(out.User.ID, p.Profile.Policy)
if !((status == 200 || status == 204) && err == nil) {
err = app.jf.SetPolicy(out.User.ID, p.Profile.Policy)
if err != nil {
app.err.Printf(lm.FailedApplyTemplate, "policy", lm.Jellyfin, out.User.ID, err)
}
status, err = app.jf.SetConfiguration(out.User.ID, p.Profile.Configuration)
if (status == 200 || status == 204) && err == nil {
status, err = app.jf.SetDisplayPreferences(out.User.ID, p.Profile.Displayprefs)
err = app.jf.SetConfiguration(out.User.ID, p.Profile.Configuration)
if err == nil {
err = app.jf.SetDisplayPreferences(out.User.ID, p.Profile.Displayprefs)
}
if !((status == 200 || status == 204) && err == nil) {
if err != nil {
app.err.Printf(lm.FailedApplyTemplate, "configuration", lm.Jellyfin, out.User.ID, err)
}
@ -164,11 +162,7 @@ func (app *appContext) SetUserDisabled(user mediabrowser.User, disabled bool) (e
change = user.Policy.IsDisabled != disabled
user.Policy.IsDisabled = disabled
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)
}
err = app.jf.SetPolicy(user.ID, user.Policy)
if err != nil {
return
}
@ -185,16 +179,12 @@ func (app *appContext) SetUserDisabled(user mediabrowser.User, disabled bool) (e
}
func (app *appContext) DeleteUser(user mediabrowser.User) (err error, deleted bool) {
var status int
if app.ombi != nil {
var tpUser map[string]any
tpUser, status, err = app.getOmbiUser(user.ID)
if status == 200 && err == nil {
tpUser, err = app.getOmbiUser(user.ID)
if err == nil {
if id, ok := tpUser["id"]; ok {
status, err = app.ombi.DeleteUser(id.(string))
if status != 200 && err == nil {
err = fmt.Errorf("failed (code %d)", status)
}
err = app.ombi.DeleteUser(id.(string))
if err != nil {
app.err.Printf(lm.FailedDeleteUser, lm.Ombi, user.ID, err)
}
@ -211,10 +201,7 @@ func (app *appContext) DeleteUser(user mediabrowser.User) (err error, deleted bo
}
}
status, err = app.jf.DeleteUser(user.ID)
if status != 200 && status != 204 && err == nil {
err = fmt.Errorf("failed (code %d)", status)
}
err = app.jf.DeleteUser(user.ID)
if err != nil {
return
}

View File

@ -17,6 +17,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
"github.com/gomarkdown/markdown"
"github.com/hrfee/jfa-go/common"
lm "github.com/hrfee/jfa-go/logmessages"
"github.com/hrfee/mediabrowser"
"github.com/lithammer/shortuuid/v3"
@ -333,26 +334,25 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
// data["pin"] = pin
// }
var resp mediabrowser.PasswordResetResponse
var status int
var err error
var username string
if !isInternal && !setPassword {
resp, status, err = app.jf.ResetPassword(pin)
resp, err = app.jf.ResetPassword(pin)
} else if time.Now().After(pwr.Expiry) {
app.debug.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", fmt.Sprintf(lm.ExpiredPIN, pin))
app.NoRouteHandler(gc)
return
} else {
status, err = app.jf.ResetPasswordAdmin(pwr.ID)
if !(status == 200 || status == 204) || err != nil {
err = app.jf.ResetPasswordAdmin(pwr.ID)
if err != nil {
app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", err)
} else {
status, err = app.jf.SetPassword(pwr.ID, "", pin)
err = app.jf.SetPassword(pwr.ID, "", pin)
}
username = pwr.Username
}
if (status == 200 || status == 204) && err == nil && (isInternal || resp.Success) {
if err == nil && (isInternal || resp.Success) {
data["success"] = true
data["pin"] = pin
if !isInternal {
@ -364,8 +364,8 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
// Only log PWRs we know the user for.
if username != "" {
jfUser, status, err := app.jf.UserByName(username, false)
if err == nil && status == 200 {
jfUser, err := app.jf.UserByName(username, false)
if err == nil {
app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityResetPassword,
UserID: jfUser.ID,
@ -377,19 +377,19 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
}
if app.config.Section("ombi").Key("enabled").MustBool(false) {
jfUser, status, err := app.jf.UserByName(username, false)
if status != 200 || err != nil {
jfUser, err := app.jf.UserByName(username, false)
if err != nil {
app.err.Printf(lm.FailedGetUser, username, lm.Jellyfin, err)
return
}
ombiUser, status, err := app.getOmbiUser(jfUser.ID)
if status != 200 || err != nil {
ombiUser, err := app.getOmbiUser(jfUser.ID)
if err != nil {
app.err.Printf(lm.FailedGetUser, username, lm.Ombi, err)
return
}
ombiUser["password"] = pin
status, err = app.ombi.ModifyUser(ombiUser)
if status != 200 || err != nil {
err = app.ombi.ModifyUser(ombiUser)
if err != nil {
app.err.Printf(lm.FailedChangePassword, lm.Ombi, ombiUser["userName"], err)
return
}
@ -539,9 +539,7 @@ func (app *appContext) verifyCaptcha(code, id, text string, isPWR bool) bool {
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
resp, err := http.DefaultClient.Do(req)
if err == nil && resp.StatusCode != 200 {
err = fmt.Errorf("failed (error %d)", resp.StatusCode)
}
err = common.GenericErr(resp.StatusCode, err)
if err != nil {
app.err.Printf(lm.FailedVerifyReCAPTCHA, err)
return false
@ -749,8 +747,8 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
fromUser := ""
if invite.ReferrerJellyfinID != "" {
sender, status, err := app.jf.UserByID(invite.ReferrerJellyfinID, false)
if status == 200 && err == nil {
sender, err := app.jf.UserByID(invite.ReferrerJellyfinID, false)
if err == nil {
fromUser = sender.Name
}
}