diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index ea15e75..0000000 --- a/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM golang:latest AS build - -COPY . /opt/build - -RUN apt update -y \ - && apt install build-essential python3-pip curl software-properties-common sed upx -y \ - && (curl -sL https://deb.nodesource.com/setup_14.x | bash -) \ - && apt install nodejs \ - && (cd /opt/build; make all; make compress) \ - && sed -i 's#id="pwrJfPath" placeholder="Folder"#id="pwrJfPath" value="/jf" disabled#g' /opt/build/build/data/templates/setup.html - -FROM golang:latest - -COPY --from=build /opt/build/build /opt/jfa-go - -EXPOSE 8056 - -CMD [ "/opt/jfa-go/jfa-go", "-data", "/data" ] - - diff --git a/LICENSE b/LICENSE deleted file mode 100644 index dc91e2a..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2020 Harvey Tindall - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/Makefile b/Makefile deleted file mode 100644 index d126cfb..0000000 --- a/Makefile +++ /dev/null @@ -1,57 +0,0 @@ -configuration: - $(info Fixing config-base) - python3 config/fixconfig.py -i config/config-base.json -o data/config-base.json - $(info Generating config-default.ini) - python3 config/generate_ini.py -i config/config-base.json -o data/config-default.ini - -sass: - $(info Getting libsass) - python3 -m pip install libsass - $(info Getting node dependencies) - npm install - $(info Compiling sass) - python3 scss/compile.py - -email: - $(info Generating email html) - python3 mail/generate.py - -typescript: - $(info Compiling typescript) - npx esbuild ts/*.ts ts/modules/*.ts --outdir=data/static --minify - -rm -r data/static/ts - -rm -r data/static/typings - -rm data/static/*.map - -ts-debug: - -npx tsc -p ts/ --sourceMap - -rm -r data/static/ts - -rm -r data/static/typings - cp -r ts data/static/ - -swagger: - go get github.com/swaggo/swag/cmd/swag - swag init -g main.go - -version: - python3 version.py auto version.go - -compile: - $(info Downloading deps) - go mod download - $(info Building) - mkdir -p build - CGO_ENABLED=0 go build -o build/jfa-go *.go - -compress: - upx --lzma build/jfa-go - -copy: - $(info Copying data) - cp -r data build/ - -install: - cp -r build $(DESTDIR)/jfa-go - -all: configuration sass email version typescript swagger compile copy -debug: configuration sass email version ts-debug swagger compile copy diff --git a/README.md b/README.md index d52f9b6..43ade54 100644 --- a/README.md +++ b/README.md @@ -1,89 +1,11 @@ -# ![jfa-go](data/static/banner.svg) +This branch is for experimenting with [a17t](https://a17t.miles.land/) to possibly replace bootstrap in the future. Currently just working on the page structures, so none of this is actually usable in jfa-go yet. -jfa-go is a user management app for [Jellyfin](https://github.com/jellyfin/jellyfin) that provides invite-based account creation as well as other features that make one's instance much easier to manage. +#### currently done: +- [x] invites tab mockup (partially complete) +- [ ] accounts tab mockup +- [ ] settings tab mockup +- [ ] modals (may not use them at all, who knows) +- [ ] animations -I chose to rewrite the python [jellyfin-accounts](https://github.com/hrfee/jellyfin-accounts) in Go mainly as a learning experience, but also to slightly improve speeds and efficiency. - -#### Features -* 🧑 Invite based account creation: Sends invites to your friends or family, and let them choose their own username and password without relying on you. - * Send invites via a link and/or email - * Granular control over invites: Validity period as well as number of uses can be specified. - * Account profiles: Assign settings profiles to invites so new users have your predefined permissions, homescreen layout, etc. applied to their account on creation. - * Password validation: Ensure users choose a strong password. -* 🔗 Ombi Integration: Automatically creates Ombi accounts for new users using their email address and login details, and your own defined set of permissions. -* Account management: Apply settings to your users individually or en masse, and delete users, optionally sending them an email notification with a reason. -* 📨 Email storage: Add your existing users email addresses through the UI, and jfa-go will ask new users for them on account creation. - * Email addresses can optionally be used instead of usernames -* 🔑 Password resets: When user's forget their passwords and request a change in Jellyfin, jfa-go reads the PIN from the created file and sends it straight to the user via email. -* Notifications: Get notified when someone creates an account, or an invite expires. -* Authentication via Jellyfin: Instead of using separate credentials for jfa-go and Jellyfin, jfa-go can use it as the authentication provider. - * Enables the usage of jfa-go by multiple people -* 🌓 Customizable look - * Specify contact and help messages to appear in emails and pages - * Light and dark themes available - * Optionally provide custom CSS - -## Interface -
- -
- -- - -
- -#### Install - -Available on the AUR as [jfa-go](https://aur.archlinux.org/packages/jfa-go/) or [jfa-go-git](https://aur.archlinux.org/packages/jfa-go-git/). - -For other platforms, grab an archive from the release section for your platform (or nightly builds [here](https://builds.hrfee.pw/view/hrfee/jfa-go)), and extract `jfa-go` and `data` to the same directory. -* For linux users, you can place them inside `/opt/jfa-go` and then run -`sudo ln -s /opt/jfa-go/jfa-go /usr/bin/jfa-go` to place it in your PATH. - -Run the executable to start. - -For [docker](https://hub.docker.com/repository/docker/hrfee/jfa-go), run: -``` -docker create \ - --name "jfa-go" \ # Whatever you want to name it - -p 8056:8056 \ - -v /path/to/.config/jfa-go:/data \ # Path to wherever you want to store the config file and other data - -v /path/to/jellyfin:/jf \ # Path to jellyfin config directory - -v /etc/localtime:/etc/localtime:ro \ # Makes sure time is correct - hrfee/jfa-go # hrfee/jfa-go:unstable for latest build from git -``` - -#### Build from source -If you're using docker, a Dockerfile is provided that builds from source. - -Otherwise, full build instructions can be found [here](https://github.com/hrfee/jfa-go/wiki/Build). - -#### Usage -Simply run `jfa-go` to start the application. A setup wizard will start on `localhost:8056` (or your own specified address). Upon completion, refresh the page. - -Note: jfa-go does not run as a daemon by default. You'll need to figure this out yourself. - -``` -Usage of ./jfa-go: - -config string - alternate path to config file. (default "~/.config/jfa-go/config.ini") - -data string - alternate path to data directory. (default "~/.config/jfa-go") - -debug - Enables debug logging and exposes pprof. - -host string - alternate address to host web ui on. - -port int - alternate port to host web ui on. - -swagger - Enable swagger at /swagger/index.html -``` - -If you're switching from jellyfin-accounts, copy your existing `~/.jf-accounts` to: - -* `XDG_CONFIG_DIR/jfa-go` (usually ~/.config/jfa-go) on \*nix systems, -* `%AppData%/jfa-go` on Windows, -* `~/Library/Application Support/jfa-go` on macOS. - -(or specify config/data path with `-config/-data` respectively.) +#### screenshots +![invites](images/invites.png) diff --git a/api.go b/api.go deleted file mode 100644 index e01920f..0000000 --- a/api.go +++ /dev/null @@ -1,1264 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "path/filepath" - "strconv" - "strings" - "time" - - "github.com/gin-gonic/gin" - "github.com/knz/strtime" - "github.com/lithammer/shortuuid/v3" - "gopkg.in/ini.v1" -) - -func respond(code int, message string, gc *gin.Context) { - resp := stringResponse{} - if code == 200 || code == 204 { - resp.Response = message - } else { - resp.Error = message - } - gc.JSON(code, resp) - gc.Abort() -} - -func respondBool(code int, val bool, gc *gin.Context) { - resp := boolResponse{} - if !val { - resp.Error = true - } else { - resp.Success = true - } - gc.JSON(code, resp) - gc.Abort() -} - -func (app *appContext) loadStrftime() { - app.datePattern = app.config.Section("email").Key("date_format").String() - app.timePattern = `%H:%M` - if val, _ := app.config.Section("email").Key("use_24h").Bool(); !val { - app.timePattern = `%I:%M %p` - } - return -} - -func (app *appContext) prettyTime(dt time.Time) (date, time string) { - date, _ = strtime.Strftime(dt, app.datePattern) - time, _ = strtime.Strftime(dt, app.timePattern) - return -} - -func (app *appContext) formatDatetime(dt time.Time) string { - d, t := app.prettyTime(dt) - return d + " " + t -} - -// https://stackoverflow.com/questions/36530251/time-since-with-months-and-years/36531443#36531443 THANKS -func timeDiff(a, b time.Time) (year, month, day, hour, min, sec int) { - if a.Location() != b.Location() { - b = b.In(a.Location()) - } - if a.After(b) { - a, b = b, a - } - y1, M1, d1 := a.Date() - y2, M2, d2 := b.Date() - - h1, m1, s1 := a.Clock() - h2, m2, s2 := b.Clock() - - year = int(y2 - y1) - month = int(M2 - M1) - day = int(d2 - d1) - hour = int(h2 - h1) - min = int(m2 - m1) - sec = int(s2 - s1) - - // Normalize negative values - if sec < 0 { - sec += 60 - min-- - } - if min < 0 { - min += 60 - hour-- - } - if hour < 0 { - hour += 24 - day-- - } - if day < 0 { - // days in month: - t := time.Date(y1, M1, 32, 0, 0, 0, 0, time.UTC) - day += 32 - t.Day() - month-- - } - if month < 0 { - month += 12 - year-- - } - return -} - -func (app *appContext) checkInvites() { - currentTime := time.Now() - app.storage.loadInvites() - changed := false - for code, data := range app.storage.invites { - expiry := data.ValidTill - if !currentTime.After(expiry) { - continue - } - app.debug.Printf("Housekeeping: Deleting old invite %s", code) - notify := data.Notify - if app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 { - app.debug.Printf("%s: Expiry notification", code) - for address, settings := range notify { - if !settings["notify-expiry"] { - continue - } - go func() { - msg, err := app.email.constructExpiry(code, data, app) - if err != nil { - app.err.Printf("%s: Failed to construct expiry notification", code) - app.debug.Printf("Error: %s", err) - } else if err := app.email.send(address, msg); err != nil { - app.err.Printf("%s: Failed to send expiry notification", code) - app.debug.Printf("Error: %s", err) - } else { - app.info.Printf("Sent expiry notification to %s", address) - } - }() - } - } - changed = true - delete(app.storage.invites, code) - } - if changed { - app.storage.storeInvites() - } -} - -func (app *appContext) checkInvite(code string, used bool, username string) bool { - currentTime := time.Now() - app.storage.loadInvites() - changed := false - inv, match := app.storage.invites[code] - if !match { - return false - } - expiry := inv.ValidTill - if currentTime.After(expiry) { - app.debug.Printf("Housekeeping: Deleting old invite %s", code) - notify := inv.Notify - if app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 { - app.debug.Printf("%s: Expiry notification", code) - for address, settings := range notify { - if settings["notify-expiry"] { - go func() { - msg, err := app.email.constructExpiry(code, inv, app) - if err != nil { - app.err.Printf("%s: Failed to construct expiry notification", code) - app.debug.Printf("Error: %s", err) - } else if err := app.email.send(address, msg); err != nil { - app.err.Printf("%s: Failed to send expiry notification", code) - app.debug.Printf("Error: %s", err) - } else { - app.info.Printf("Sent expiry notification to %s", address) - } - }() - } - } - } - changed = true - match = false - delete(app.storage.invites, code) - } else if used { - changed = true - del := false - newInv := inv - if newInv.RemainingUses == 1 { - del = true - delete(app.storage.invites, code) - } else if newInv.RemainingUses != 0 { - // 0 means infinite i guess? - newInv.RemainingUses -= 1 - } - newInv.UsedBy = append(newInv.UsedBy, []string{username, app.formatDatetime(currentTime)}) - if !del { - app.storage.invites[code] = newInv - } - } - if changed { - app.storage.storeInvites() - } - return match -} - -func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, int, error) { - ombiUsers, code, err := app.ombi.GetUsers() - if err != nil || code != 200 { - return nil, code, err - } - jfUser, code, err := app.jf.UserByID(jfID, false) - if err != nil || code != 200 { - return nil, code, err - } - username := jfUser["Name"].(string) - email := "" - if e, ok := app.storage.emails[jfID]; ok { - email = e.(string) - } - for _, ombiUser := range ombiUsers { - ombiAddr := "" - if a, ok := ombiUser["emailAddress"]; ok && a != nil { - ombiAddr = a.(string) - } - if ombiUser["userName"].(string) == username || (ombiAddr == email && email != "") { - return ombiUser, code, err - } - } - return nil, 400, fmt.Errorf("Couldn't find user") -} - -// Routes from now on! - -// @Summary Creates a new Jellyfin user without an invite. -// @Produce json -// @Param newUserDTO body newUserDTO true "New user request object" -// @Success 200 -// @Router /users [post] -// @Security Bearer -// @tags Users -func (app *appContext) NewUserAdmin(gc *gin.Context) { - var req newUserDTO - gc.BindJSON(&req) - existingUser, _, _ := app.jf.UserByName(req.Username, false) - if existingUser != nil { - msg := fmt.Sprintf("User already exists named %s", req.Username) - app.info.Printf("%s New user failed: %s", req.Username, msg) - respond(401, msg, gc) - return - } - user, status, err := app.jf.NewUser(req.Username, req.Password) - if !(status == 200 || status == 204) || err != nil { - app.err.Printf("%s New user failed: Jellyfin responded with %d", req.Username, status) - respond(401, "Unknown error", gc) - return - } - var id string - if user["Id"] != nil { - id = user["Id"].(string) - } - if len(app.storage.policy) != 0 { - status, err = app.jf.SetPolicy(id, app.storage.policy) - if !(status == 200 || status == 204 || err == nil) { - app.err.Printf("%s: Failed to set user policy: Code %d", req.Username, status) - app.debug.Printf("%s: Error: %s", req.Username, err) - } - } - if len(app.storage.configuration) != 0 && len(app.storage.displayprefs) != 0 { - status, err = app.jf.SetConfiguration(id, app.storage.configuration) - if (status == 200 || status == 204) && err == nil { - status, err = app.jf.SetDisplayPreferences(id, app.storage.displayprefs) - } - if !((status == 200 || status == 204) && err == nil) { - app.err.Printf("%s: Failed to set configuration template: Code %d", req.Username, status) - } - } - if app.config.Section("password_resets").Key("enabled").MustBool(false) { - app.storage.emails[id] = req.Email - app.storage.storeEmails() - } - if app.config.Section("ombi").Key("enabled").MustBool(false) { - app.storage.loadOmbiTemplate() - if len(app.storage.ombi_template) != 0 { - errors, code, err := app.ombi.NewUser(req.Username, req.Password, req.Email, app.storage.ombi_template) - if err != nil || code != 200 { - app.info.Printf("Failed to create Ombi user (%d): %s", code, err) - app.debug.Printf("Errors reported by Ombi: %s", strings.Join(errors, ", ")) - } else { - app.info.Println("Created Ombi user") - } - } - } - app.jf.CacheExpiry = time.Now() -} - -// @Summary Creates a new Jellyfin user via invite code -// @Produce json -// @Param newUserDTO body newUserDTO true "New user request object" -// @Success 200 {object} PasswordValidation -// @Failure 400 {object} PasswordValidation -// @Router /newUser [post] -// @tags Users -func (app *appContext) NewUser(gc *gin.Context) { - var req newUserDTO - gc.BindJSON(&req) - app.debug.Printf("%s: New user attempt", req.Code) - if !app.checkInvite(req.Code, false, "") { - app.info.Printf("%s New user failed: invalid code", req.Code) - respondBool(401, false, gc) - return - } - validation := app.validator.validate(req.Password) - valid := true - for _, val := range validation { - if !val { - valid = false - } - } - if !valid { - // 200 bcs idk what i did in js - app.info.Printf("%s New user failed: Invalid password", req.Code) - gc.JSON(200, validation) - gc.Abort() - return - } - existingUser, _, _ := app.jf.UserByName(req.Username, false) - if existingUser != nil { - msg := fmt.Sprintf("User already exists named %s", req.Username) - app.info.Printf("%s New user failed: %s", req.Code, msg) - respond(401, msg, gc) - return - } - user, status, err := app.jf.NewUser(req.Username, req.Password) - if !(status == 200 || status == 204) || err != nil { - app.err.Printf("%s New user failed: Jellyfin responded with %d", req.Code, status) - respond(401, "Unknown error", gc) - return - } - app.storage.loadProfiles() - invite := app.storage.invites[req.Code] - app.checkInvite(req.Code, true, req.Username) - if app.config.Section("notifications").Key("enabled").MustBool(false) { - for address, settings := range invite.Notify { - if settings["notify-creation"] { - go func() { - msg, err := app.email.constructCreated(req.Code, req.Username, req.Email, invite, app) - if err != nil { - app.err.Printf("%s: Failed to construct user creation notification", req.Code) - app.debug.Printf("%s: Error: %s", req.Code, err) - } else if err := app.email.send(address, msg); err != nil { - app.err.Printf("%s: Failed to send user creation notification", req.Code) - app.debug.Printf("%s: Error: %s", req.Code, err) - } else { - app.info.Printf("%s: Sent user creation notification to %s", req.Code, address) - } - }() - } - } - } - var id string - if user["Id"] != nil { - id = user["Id"].(string) - } - if invite.Profile != "" { - app.debug.Printf("Applying settings from profile \"%s\"", invite.Profile) - profile, ok := app.storage.profiles[invite.Profile] - if !ok { - profile = app.storage.profiles["Default"] - } - if len(profile.Policy) != 0 { - app.debug.Printf("Applying policy from profile \"%s\"", invite.Profile) - status, err = app.jf.SetPolicy(id, profile.Policy) - if !((status == 200 || status == 204) && err == nil) { - app.err.Printf("%s: Failed to set user policy: Code %d", req.Code, status) - app.debug.Printf("%s: Error: %s", req.Code, err) - } - } - if len(profile.Configuration) != 0 && len(profile.Displayprefs) != 0 { - app.debug.Printf("Applying homescreen from profile \"%s\"", invite.Profile) - status, err = app.jf.SetConfiguration(id, profile.Configuration) - if (status == 200 || status == 204) && err == nil { - status, err = app.jf.SetDisplayPreferences(id, profile.Displayprefs) - } - if !((status == 200 || status == 204) && err == nil) { - app.err.Printf("%s: Failed to set configuration template: Code %d", req.Code, status) - app.debug.Printf("%s: Error: %s", req.Code, err) - } - } - } - // if app.config.Section("password_resets").Key("enabled").MustBool(false) { - if req.Email != "" { - app.storage.emails[id] = req.Email - app.storage.storeEmails() - } - if app.config.Section("ombi").Key("enabled").MustBool(false) { - app.storage.loadOmbiTemplate() - if len(app.storage.ombi_template) != 0 { - errors, code, err := app.ombi.NewUser(req.Username, req.Password, req.Email, app.storage.ombi_template) - if err != nil || code != 200 { - app.info.Printf("Failed to create Ombi user (%d): %s", code, err) - app.debug.Printf("Errors reported by Ombi: %s", strings.Join(errors, ", ")) - } else { - app.info.Println("Created Ombi user") - } - } - } - code := 200 - for _, val := range validation { - if !val { - code = 400 - } - } - gc.JSON(code, validation) -} - -// @Summary Delete a list of users, optionally notifying them why. -// @Produce json -// @Param deleteUserDTO body deleteUserDTO true "User deletion request object" -// @Success 200 {object} boolResponse -// @Failure 400 {object} stringResponse -// @Failure 500 {object} errorListDTO "List of errors" -// @Router /users [delete] -// @Security Bearer -// @tags Users -func (app *appContext) DeleteUser(gc *gin.Context) { - var req deleteUserDTO - gc.BindJSON(&req) - errors := map[string]string{} - ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false) - for _, userID := range req.Users { - if ombiEnabled { - ombiUser, code, err := app.getOmbiUser(userID) - if code == 200 && err == nil { - if id, ok := ombiUser["id"]; ok { - status, err := app.ombi.DeleteUser(id.(string)) - if err != nil || status != 200 { - app.err.Printf("Failed to delete ombi user: %d %s", status, err) - errors[userID] = fmt.Sprintf("Ombi: %d %s, ", status, err) - } - } - } - } - status, err := app.jf.DeleteUser(userID) - if !(status == 200 || status == 204) || err != nil { - msg := fmt.Sprintf("%d: %s", status, err) - if _, ok := errors[userID]; !ok { - errors[userID] = msg - } else { - errors[userID] += msg - } - } - if req.Notify { - addr, ok := app.storage.emails[userID] - if addr != nil && ok { - go func(userID, reason, address string) { - msg, err := app.email.constructDeleted(reason, app) - if err != nil { - app.err.Printf("%s: Failed to construct account deletion email", userID) - app.debug.Printf("%s: Error: %s", userID, err) - } else if err := app.email.send(address, msg); err != nil { - app.err.Printf("%s: Failed to send to %s", userID, address) - app.debug.Printf("%s: Error: %s", userID, err) - } else { - app.info.Printf("%s: Sent invite email to %s", userID, address) - } - }(userID, req.Reason, addr.(string)) - } - } - } - app.jf.CacheExpiry = time.Now() - if len(errors) == len(req.Users) { - respondBool(500, false, gc) - app.err.Printf("Account deletion failed: %s", errors[req.Users[0]]) - return - } else if len(errors) != 0 { - gc.JSON(500, errors) - return - } - respondBool(200, true, gc) -} - -// @Summary Create a new invite. -// @Produce json -// @Param generateInviteDTO body generateInviteDTO true "New invite request object" -// @Success 200 {object} boolResponse -// @Router /invites [post] -// @Security Bearer -// @tags Invites -func (app *appContext) GenerateInvite(gc *gin.Context) { - var req generateInviteDTO - app.debug.Println("Generating new invite") - app.storage.loadInvites() - gc.BindJSON(&req) - currentTime := time.Now() - validTill := currentTime.AddDate(0, 0, req.Days) - validTill = validTill.Add(time.Hour*time.Duration(req.Hours) + time.Minute*time.Duration(req.Minutes)) - // make sure code doesn't begin with number - inviteCode := shortuuid.New() - _, err := strconv.Atoi(string(inviteCode[0])) - for err == nil { - inviteCode = shortuuid.New() - _, err = strconv.Atoi(string(inviteCode[0])) - } - var invite Invite - invite.Created = currentTime - if req.MultipleUses { - if req.NoLimit { - invite.NoLimit = true - } else { - invite.RemainingUses = req.RemainingUses - } - } else { - invite.RemainingUses = 1 - } - invite.ValidTill = validTill - if req.Email != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) { - app.debug.Printf("%s: Sending invite email", inviteCode) - invite.Email = req.Email - msg, err := app.email.constructInvite(inviteCode, invite, app) - if err != nil { - invite.Email = fmt.Sprintf("Failed to send to %s", req.Email) - app.err.Printf("%s: Failed to construct invite email", inviteCode) - app.debug.Printf("%s: Error: %s", inviteCode, err) - } else if err := app.email.send(req.Email, msg); err != nil { - invite.Email = fmt.Sprintf("Failed to send to %s", req.Email) - app.err.Printf("%s: %s", inviteCode, invite.Email) - app.debug.Printf("%s: Error: %s", inviteCode, err) - } else { - app.info.Printf("%s: Sent invite email to %s", inviteCode, req.Email) - } - } - if req.Profile != "" { - if _, ok := app.storage.profiles[req.Profile]; ok { - invite.Profile = req.Profile - } else { - invite.Profile = "Default" - } - } - app.storage.invites[inviteCode] = invite - app.storage.storeInvites() - respondBool(200, true, gc) -} - -// @Summary Set profile for an invite -// @Produce json -// @Param inviteProfileDTO body inviteProfileDTO true "Invite profile object" -// @Success 200 {object} boolResponse -// @Failure 500 {object} stringResponse -// @Router /invites/profile [post] -// @Security Bearer -// @tags Profiles & Settings -func (app *appContext) SetProfile(gc *gin.Context) { - var req inviteProfileDTO - gc.BindJSON(&req) - app.debug.Printf("%s: Setting profile to \"%s\"", req.Invite, req.Profile) - // "" means "Don't apply profile" - if _, ok := app.storage.profiles[req.Profile]; !ok && req.Profile != "" { - app.err.Printf("%s: Profile \"%s\" not found", req.Invite, req.Profile) - respond(500, "Profile not found", gc) - return - } - inv := app.storage.invites[req.Invite] - inv.Profile = req.Profile - app.storage.invites[req.Invite] = inv - app.storage.storeInvites() - respondBool(200, true, gc) -} - -// @Summary Get a list of profiles -// @Produce json -// @Success 200 {object} getProfilesDTO -// @Router /profiles [get] -// @Security Bearer -// @tags Profiles & Settings -func (app *appContext) GetProfiles(gc *gin.Context) { - app.storage.loadProfiles() - app.debug.Println("Profiles requested") - out := getProfilesDTO{ - DefaultProfile: app.storage.defaultProfile, - Profiles: map[string]profileDTO{}, - } - for name, p := range app.storage.profiles { - out.Profiles[name] = profileDTO{ - Admin: p.Admin, - LibraryAccess: p.LibraryAccess, - FromUser: p.FromUser, - } - } - gc.JSON(200, out) -} - -// @Summary Set the default profile to use. -// @Produce json -// @Param profileChangeDTO body profileChangeDTO true "Default profile object" -// @Success 200 {object} boolResponse -// @Failure 500 {object} stringResponse -// @Router /profiles/default [post] -// @Security Bearer -// @tags Profiles & Settings -func (app *appContext) SetDefaultProfile(gc *gin.Context) { - req := profileChangeDTO{} - gc.BindJSON(&req) - app.info.Printf("Setting default profile to \"%s\"", req.Name) - if _, ok := app.storage.profiles[req.Name]; !ok { - app.err.Printf("Profile not found: \"%s\"", req.Name) - respond(500, "Profile not found", gc) - return - } - for name, profile := range app.storage.profiles { - if name == req.Name { - profile.Admin = true - app.storage.profiles[name] = profile - } else { - profile.Admin = false - } - } - app.storage.defaultProfile = req.Name - respondBool(200, true, gc) -} - -// @Summary Create a profile based on a Jellyfin user's settings. -// @Produce json -// @Param newProfileDTO body newProfileDTO true "New profile object" -// @Success 200 {object} boolResponse -// @Failure 500 {object} stringResponse -// @Router /profiles [post] -// @Security Bearer -// @tags Profiles & Settings -func (app *appContext) CreateProfile(gc *gin.Context) { - app.info.Println("Profile creation requested") - var req newProfileDTO - gc.BindJSON(&req) - user, status, err := app.jf.UserByID(req.ID, false) - if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Failed to get user from Jellyfin: Code %d", status) - app.debug.Printf("Error: %s", err) - respond(500, "Couldn't get user", gc) - return - } - profile := Profile{ - FromUser: user["Name"].(string), - Policy: user["Policy"].(map[string]interface{}), - } - app.debug.Printf("Creating profile from user \"%s\"", user["Name"].(string)) - if req.Homescreen { - profile.Configuration = user["Configuration"].(map[string]interface{}) - profile.Displayprefs, status, err = app.jf.GetDisplayPreferences(req.ID) - if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Failed to get DisplayPrefs: Code %d", status) - app.debug.Printf("Error: %s", err) - respond(500, "Couldn't get displayprefs", gc) - return - } - } - app.storage.loadProfiles() - app.storage.profiles[req.Name] = profile - app.storage.storeProfiles() - app.storage.loadProfiles() - respondBool(200, true, gc) -} - -// @Summary Delete an existing profile -// @Produce json -// @Param profileChangeDTO body profileChangeDTO true "Delete profile object" -// @Success 200 {object} boolResponse -// @Router /profiles [delete] -// @Security Bearer -// @tags Profiles & Settings -func (app *appContext) DeleteProfile(gc *gin.Context) { - req := profileChangeDTO{} - gc.BindJSON(&req) - name := req.Name - if _, ok := app.storage.profiles[name]; ok { - delete(app.storage.profiles, name) - } - app.storage.storeProfiles() - respondBool(200, true, gc) -} - -// @Summary Get invites. -// @Produce json -// @Success 200 {object} getInvitesDTO -// @Router /invites [get] -// @Security Bearer -// @tags Invites -func (app *appContext) GetInvites(gc *gin.Context) { - app.debug.Println("Invites requested") - currentTime := time.Now() - app.storage.loadInvites() - app.checkInvites() - var invites []inviteDTO - for code, inv := range app.storage.invites { - _, _, days, hours, minutes, _ := timeDiff(inv.ValidTill, currentTime) - invite := inviteDTO{ - Code: code, - Days: days, - Hours: hours, - Minutes: minutes, - Created: app.formatDatetime(inv.Created), - Profile: inv.Profile, - NoLimit: inv.NoLimit, - } - if len(inv.UsedBy) != 0 { - invite.UsedBy = inv.UsedBy - } - invite.RemainingUses = 1 - if inv.RemainingUses != 0 { - invite.RemainingUses = inv.RemainingUses - } - if inv.Email != "" { - invite.Email = inv.Email - } - if len(inv.Notify) != 0 { - var address string - if app.config.Section("ui").Key("jellyfin_login").MustBool(false) { - app.storage.loadEmails() - if addr := app.storage.emails[gc.GetString("jfId")]; addr != nil { - address = addr.(string) - } - } else { - address = app.config.Section("ui").Key("email").String() - } - if _, ok := inv.Notify[address]; ok { - if _, ok = inv.Notify[address]["notify-expiry"]; ok { - invite.NotifyExpiry = inv.Notify[address]["notify-expiry"] - } - if _, ok = inv.Notify[address]["notify-creation"]; ok { - invite.NotifyCreation = inv.Notify[address]["notify-creation"] - } - } - } - invites = append(invites, invite) - } - profiles := make([]string, len(app.storage.profiles)) - if len(app.storage.profiles) != 0 { - profiles[0] = app.storage.defaultProfile - i := 1 - if len(app.storage.profiles) > 1 { - for p := range app.storage.profiles { - if p != app.storage.defaultProfile { - profiles[i] = p - i++ - } - } - } - } - resp := getInvitesDTO{ - Profiles: profiles, - Invites: invites, - } - gc.JSON(200, resp) -} - -// @Summary Set notification preferences for an invite. -// @Produce json -// @Param setNotifyDTO body setNotifyDTO true "Map of invite codes to notification settings objects" -// @Success 200 -// @Failure 400 {object} stringResponse -// @Failure 500 {object} stringResponse -// @Router /invites/notify [post] -// @Security Bearer -// @tags Other -func (app *appContext) SetNotify(gc *gin.Context) { - var req map[string]map[string]bool - gc.BindJSON(&req) - changed := false - for code, settings := range req { - app.debug.Printf("%s: Notification settings change requested", code) - app.storage.loadInvites() - app.storage.loadEmails() - invite, ok := app.storage.invites[code] - if !ok { - app.err.Printf("%s Notification setting change failed: Invalid code", code) - respond(400, "Invalid invite code", gc) - return - } - var address string - if app.config.Section("ui").Key("jellyfin_login").MustBool(false) { - var ok bool - address, ok = app.storage.emails[gc.GetString("jfId")].(string) - if !ok { - app.err.Printf("%s: Couldn't find email address. Make sure it's set", code) - app.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId")) - respond(500, "Missing user email", gc) - return - } - } else { - address = app.config.Section("ui").Key("email").String() - } - if invite.Notify == nil { - invite.Notify = map[string]map[string]bool{} - } - if _, ok := invite.Notify[address]; !ok { - invite.Notify[address] = map[string]bool{} - } /*else { - if _, ok := invite.Notify[address]["notify-expiry"]; !ok { - */ - if _, ok := settings["notify-expiry"]; ok && invite.Notify[address]["notify-expiry"] != settings["notify-expiry"] { - invite.Notify[address]["notify-expiry"] = settings["notify-expiry"] - app.debug.Printf("%s: Set \"notify-expiry\" to %t for %s", code, settings["notify-expiry"], address) - changed = true - } - if _, ok := settings["notify-creation"]; ok && invite.Notify[address]["notify-creation"] != settings["notify-creation"] { - invite.Notify[address]["notify-creation"] = settings["notify-creation"] - app.debug.Printf("%s: Set \"notify-creation\" to %t for %s", code, settings["notify-creation"], address) - changed = true - } - if changed { - app.storage.invites[code] = invite - } - } - if changed { - app.storage.storeInvites() - } -} - -// @Summary Delete an invite. -// @Produce json -// @Param deleteInviteDTO body deleteInviteDTO true "Delete invite object" -// @Success 200 {object} boolResponse -// @Failure 400 {object} stringResponse -// @Router /invites [delete] -// @Security Bearer -// @tags Invites -func (app *appContext) DeleteInvite(gc *gin.Context) { - var req deleteInviteDTO - gc.BindJSON(&req) - app.debug.Printf("%s: Deletion requested", req.Code) - var ok bool - _, ok = app.storage.invites[req.Code] - if ok { - delete(app.storage.invites, req.Code) - app.storage.storeInvites() - app.info.Printf("%s: Invite deleted", req.Code) - respondBool(200, true, gc) - return - } - app.err.Printf("%s: Deletion failed: Invalid code", req.Code) - respond(400, "Code doesn't exist", gc) -} - -type dateToParse struct { - Parsed time.Time `json:"parseme"` -} - -func parseDT(date string) time.Time { - // decent method - dt, err := time.Parse("2006-01-02T15:04:05.000000", date) - if err == nil { - return dt - } - // magic method - // some stored dates from jellyfin have no timezone at the end, if not we assume UTC - if date[len(date)-1] != 'Z' { - date += "Z" - } - timeJSON := []byte("{ \"parseme\": \"" + date + "\" }") - var parsed dateToParse - // Magically turn it into a time.Time - json.Unmarshal(timeJSON, &parsed) - return parsed.Parsed -} - -// @Summary Get a list of Jellyfin users. -// @Produce json -// @Success 200 {object} getUsersDTO -// @Failure 500 {object} stringResponse -// @Router /users [get] -// @Security Bearer -// @tags Users -func (app *appContext) GetUsers(gc *gin.Context) { - app.debug.Println("Users requested") - var resp getUsersDTO - resp.UserList = []respUser{} - users, status, err := app.jf.GetUsers(false) - if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Failed to get users from Jellyfin: Code %d", status) - app.debug.Printf("Error: %s", err) - respond(500, "Couldn't get users", gc) - return - } - for _, jfUser := range users { - var user respUser - user.LastActive = "n/a" - if jfUser["LastActivityDate"] != nil { - date := parseDT(jfUser["LastActivityDate"].(string)) - user.LastActive = app.formatDatetime(date) - // fmt.Printf("%s: %s, %s, %+v\n", jfUser["Name"].(string), jfUser["LastActivityDate"].(string), user.LastActive, date) - } - user.ID = jfUser["Id"].(string) - user.Name = jfUser["Name"].(string) - user.Admin = jfUser["Policy"].(map[string]interface{})["IsAdministrator"].(bool) - if email, ok := app.storage.emails[jfUser["Id"].(string)]; ok { - user.Email = email.(string) - } - - resp.UserList = append(resp.UserList, user) - } - gc.JSON(200, resp) -} - -// @Summary Get a list of Ombi users. -// @Produce json -// @Success 200 {object} ombiUsersDTO -// @Failure 500 {object} stringResponse -// @Router /ombi/users [get] -// @Security Bearer -// @tags Ombi -func (app *appContext) OmbiUsers(gc *gin.Context) { - app.debug.Println("Ombi users requested") - users, status, err := app.ombi.GetUsers() - if err != nil || status != 200 { - app.err.Printf("Failed to get users from Ombi: Code %d", status) - app.debug.Printf("Error: %s", err) - respond(500, "Couldn't get users", gc) - return - } - userlist := make([]ombiUser, len(users)) - for i, data := range users { - userlist[i] = ombiUser{ - Name: data["userName"].(string), - ID: data["id"].(string), - } - } - gc.JSON(200, ombiUsersDTO{Users: userlist}) -} - -// @Summary Set new user defaults for Ombi accounts. -// @Produce json -// @Param ombiUser body ombiUser true "User to source settings from" -// @Success 200 {object} boolResponse -// @Failure 500 {object} stringResponse -// @Router /ombi/defaults [post] -// @Security Bearer -// @tags Ombi -func (app *appContext) SetOmbiDefaults(gc *gin.Context) { - var req ombiUser - gc.BindJSON(&req) - template, code, err := app.ombi.TemplateByID(req.ID) - if err != nil || code != 200 || len(template) == 0 { - app.err.Printf("Couldn't get user from Ombi: %d %s", code, err) - respond(500, "Couldn't get user", gc) - return - } - app.storage.ombi_template = template - app.storage.storeOmbiTemplate() - respondBool(200, true, gc) -} - -// @Summary Modify user's email addresses. -// @Produce json -// @Param modifyEmailsDTO body modifyEmailsDTO true "Map of userIDs to email addresses" -// @Success 200 {object} boolResponse -// @Failure 500 {object} stringResponse -// @Router /users/emails [post] -// @Security Bearer -// @tags Users -func (app *appContext) ModifyEmails(gc *gin.Context) { - var req modifyEmailsDTO - gc.BindJSON(&req) - app.debug.Println("Email modification requested") - users, status, err := app.jf.GetUsers(false) - if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Failed to get users from Jellyfin: Code %d", status) - app.debug.Printf("Error: %s", err) - respond(500, "Couldn't get users", gc) - return - } - ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false) - for _, jfUser := range users { - id := jfUser["Id"].(string) - if address, ok := req[id]; ok { - app.storage.emails[jfUser["Id"].(string)] = address - if ombiEnabled { - ombiUser, code, err := app.getOmbiUser(id) - if code == 200 && err == nil { - ombiUser["emailAddress"] = address - code, err = app.ombi.ModifyUser(ombiUser) - if code != 200 || err != nil { - app.err.Printf("%s: Failed to change ombi email address: %d %s", ombiUser["userName"].(string), code, err) - } - } - } - } - } - app.storage.storeEmails() - app.info.Println("Email list modified") - respondBool(200, true, gc) -} - -// @Summary Apply settings to a list of users, either from a profile or from another user. -// @Produce json -// @Param userSettingsDTO body userSettingsDTO true "Parameters for applying settings" -// @Success 200 {object} errorListDTO -// @Failure 500 {object} errorListDTO "Lists of errors that occurred while applying settings" -// @Router /users/settings [post] -// @Security Bearer -// @tags Profiles & Settings -func (app *appContext) ApplySettings(gc *gin.Context) { - app.info.Println("User settings change requested") - var req userSettingsDTO - gc.BindJSON(&req) - applyingFrom := "profile" - var policy, configuration, displayprefs map[string]interface{} - if req.From == "profile" { - app.storage.loadProfiles() - if _, ok := app.storage.profiles[req.Profile]; !ok || len(app.storage.profiles[req.Profile].Policy) == 0 { - app.err.Printf("Couldn't find profile \"%s\" or profile was empty", req.Profile) - respond(500, "Couldn't find profile", gc) - return - } - if req.Homescreen { - if len(app.storage.profiles[req.Profile].Configuration) == 0 || len(app.storage.profiles[req.Profile].Displayprefs) == 0 { - app.err.Printf("No homescreen saved in profile \"%s\"", req.Profile) - respond(500, "No homescreen template available", gc) - return - } - configuration = app.storage.profiles[req.Profile].Configuration - displayprefs = app.storage.profiles[req.Profile].Displayprefs - } - policy = app.storage.profiles[req.Profile].Policy - } else if req.From == "user" { - applyingFrom = "user" - user, status, err := app.jf.UserByID(req.ID, false) - if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Failed to get user from Jellyfin: Code %d", status) - app.debug.Printf("Error: %s", err) - respond(500, "Couldn't get user", gc) - return - } - applyingFrom = "\"" + user["Name"].(string) + "\"" - policy = user["Policy"].(map[string]interface{}) - if req.Homescreen { - displayprefs, status, err = app.jf.GetDisplayPreferences(req.ID) - if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Failed to get DisplayPrefs: Code %d", status) - app.debug.Printf("Error: %s", err) - respond(500, "Couldn't get displayprefs", gc) - return - } - configuration = user["Configuration"].(map[string]interface{}) - } - } - app.info.Printf("Applying settings to %d user(s) from %s", len(req.ApplyTo), applyingFrom) - errors := errorListDTO{ - "policy": map[string]string{}, - "homescreen": map[string]string{}, - } - for _, id := range req.ApplyTo { - status, err := app.jf.SetPolicy(id, policy) - if !(status == 200 || status == 204) || err != nil { - errors["policy"][id] = fmt.Sprintf("%d: %s", status, err) - } - if req.Homescreen { - status, err = app.jf.SetConfiguration(id, configuration) - errorString := "" - if !(status == 200 || status == 204) || err != nil { - errorString += fmt.Sprintf("Configuration %d: %s ", status, err) - } else { - status, err = app.jf.SetDisplayPreferences(id, displayprefs) - if !(status == 200 || status == 204) || err != nil { - errorString += fmt.Sprintf("Displayprefs %d: %s ", status, err) - } - } - if errorString != "" { - errors["homescreen"][id] = errorString - } - } - } - code := 200 - if len(errors["policy"]) == len(req.ApplyTo) || len(errors["homescreen"]) == len(req.ApplyTo) { - code = 500 - } - gc.JSON(code, errors) -} - -// @Summary Get jfa-go configuration. -// @Produce json -// @Success 200 {object} configDTO "Uses the same format as config-base.json" -// @Router /config [get] -// @Security Bearer -// @tags Configuration -func (app *appContext) GetConfig(gc *gin.Context) { - app.info.Println("Config requested") - resp := map[string]interface{}{} - langPath := filepath.Join(app.localPath, "lang", "form") - app.lang.langFiles, _ = ioutil.ReadDir(langPath) - app.lang.langOptions = make([]string, len(app.lang.langFiles)) - chosenLang := app.config.Section("ui").Key("language").MustString("en-us") + ".json" - for i, f := range app.lang.langFiles { - if f.Name() == chosenLang { - app.lang.chosenIndex = i - } - var langFile map[string]interface{} - file, _ := ioutil.ReadFile(filepath.Join(langPath, f.Name())) - json.Unmarshal(file, &langFile) - - if meta, ok := langFile["meta"]; ok { - app.lang.langOptions[i] = meta.(map[string]interface{})["name"].(string) - } - } - for section, settings := range app.configBase { - if section == "order" { - resp[section] = settings.([]interface{}) - } else { - resp[section] = make(map[string]interface{}) - for key, values := range settings.(map[string]interface{}) { - if key == "order" { - resp[section].(map[string]interface{})[key] = values.([]interface{}) - } else { - resp[section].(map[string]interface{})[key] = values.(map[string]interface{}) - if key != "meta" { - dataType := resp[section].(map[string]interface{})[key].(map[string]interface{})["type"].(string) - configKey := app.config.Section(section).Key(key) - if dataType == "number" { - if val, err := configKey.Int(); err == nil { - resp[section].(map[string]interface{})[key].(map[string]interface{})["value"] = val - } - } else if dataType == "bool" { - resp[section].(map[string]interface{})[key].(map[string]interface{})["value"] = configKey.MustBool(false) - } else if dataType == "select" && key == "language" { - resp[section].(map[string]interface{})[key].(map[string]interface{})["options"] = app.lang.langOptions - resp[section].(map[string]interface{})[key].(map[string]interface{})["value"] = app.lang.langOptions[app.lang.chosenIndex] - } else { - resp[section].(map[string]interface{})[key].(map[string]interface{})["value"] = configKey.String() - } - } - } - } - } - } - // resp["jellyfin"].(map[string]interface{})["language"].(map[string]interface{})["options"].([]string) - gc.JSON(200, resp) -} - -// @Summary Modify app config. -// @Produce json -// @Param appConfig body configDTO true "Config split into sections as in config.ini, all values as strings." -// @Success 200 {object} boolResponse -// @Router /config [post] -// @Security Bearer -// @tags Configuration -func (app *appContext) ModifyConfig(gc *gin.Context) { - app.info.Println("Config modification requested") - var req configDTO - gc.BindJSON(&req) - tempConfig, _ := ini.Load(app.configPath) - for section, settings := range req { - if section != "restart-program" { - _, err := tempConfig.GetSection(section) - if err != nil { - tempConfig.NewSection(section) - } - for setting, value := range settings.(map[string]interface{}) { - if section == "ui" && setting == "language" { - for i, lang := range app.lang.langOptions { - if value.(string) == lang { - tempConfig.Section(section).Key(setting).SetValue(strings.Replace(app.lang.langFiles[i].Name(), ".json", "", 1)) - break - } - } - } else { - tempConfig.Section(section).Key(setting).SetValue(value.(string)) - } - } - } - } - tempConfig.SaveTo(app.configPath) - app.debug.Println("Config saved") - gc.JSON(200, map[string]bool{"success": true}) - if req["restart-program"] != nil && req["restart-program"].(bool) { - app.info.Println("Restarting...") - err := app.Restart() - if err != nil { - app.err.Printf("Couldn't restart, try restarting manually. (%s)", err) - } - } - app.loadConfig() - // Reinitialize password validator on config change, as opposed to every applicable request like in python. - if _, ok := req["password_validation"]; ok { - app.debug.Println("Reinitializing validator") - validatorConf := ValidatorConf{ - "characters": app.config.Section("password_validation").Key("min_length").MustInt(0), - "uppercase characters": app.config.Section("password_validation").Key("upper").MustInt(0), - "lowercase characters": app.config.Section("password_validation").Key("lower").MustInt(0), - "numbers": app.config.Section("password_validation").Key("number").MustInt(0), - "special characters": app.config.Section("password_validation").Key("special").MustInt(0), - } - if !app.config.Section("password_validation").Key("enabled").MustBool(false) { - for key := range validatorConf { - validatorConf[key] = 0 - } - } - app.validator.init(validatorConf) - } -} - -// @Summary Logout by deleting refresh token from cookies. -// @Produce json -// @Success 200 {object} boolResponse -// @Failure 500 {object} stringResponse -// @Router /logout [post] -// @tags Other -func (app *appContext) Logout(gc *gin.Context) { - cookie, err := gc.Cookie("refresh") - if err != nil { - app.debug.Printf("Couldn't get cookies: %s", err) - respond(500, "Couldn't fetch cookies", gc) - return - } - app.invalidTokens = append(app.invalidTokens, cookie) - gc.SetCookie("refresh", "invalid", -1, "/", gc.Request.URL.Hostname(), true, true) - respondBool(200, true, gc) -} - -// func Restart() error { -// defer func() { -// if r := recover(); r != nil { -// os.Exit(0) -// } -// }() -// cwd, err := os.Getwd() -// if err != nil { -// return err -// } -// args := os.Args -// // for _, key := range args { -// // fmt.Println(key) -// // } -// cmd := exec.Command(args[0], args[1:]...) -// cmd.Stdout = os.Stdout -// cmd.Stderr = os.Stderr -// cmd.Dir = cwd -// err = cmd.Start() -// if err != nil { -// return err -// } -// // cmd.Process.Release() -// panic(fmt.Errorf("restarting")) -// } - -// func (app *appContext) Restart() error { -// defer func() { -// if r := recover(); r != nil { -// signal.Notify(app.quit, os.Interrupt) -// <-app.quit -// } -// }() -// args := os.Args -// // After a single restart, args[0] gets messed up and isnt the real executable. -// // JFA_DEEP tells the new process its a child, and JFA_EXEC is the real executable -// if os.Getenv("JFA_DEEP") == "" { -// os.Setenv("JFA_DEEP", "1") -// os.Setenv("JFA_EXEC", args[0]) -// } -// env := os.Environ() -// err := syscall.Exec(os.Getenv("JFA_EXEC"), []string{""}, env) -// if err != nil { -// return err -// } -// panic(fmt.Errorf("restarting")) -// } - -// no need to syscall.exec anymore! -func (app *appContext) Restart() error { - RESTART <- true - return nil -} diff --git a/auth.go b/auth.go deleted file mode 100644 index fc927c4..0000000 --- a/auth.go +++ /dev/null @@ -1,228 +0,0 @@ -package main - -import ( - "encoding/base64" - "fmt" - "os" - "strconv" - "strings" - "time" - - "github.com/dgrijalva/jwt-go" - "github.com/gin-gonic/gin" - "github.com/lithammer/shortuuid/v3" -) - -func (app *appContext) webAuth() gin.HandlerFunc { - return app.authenticate -} - -// CreateToken returns a web token as well as a refresh token, which can be used to obtain new tokens. -func CreateToken(userId, jfId string) (string, string, error) { - var token, refresh string - claims := jwt.MapClaims{ - "valid": true, - "id": userId, - "exp": strconv.FormatInt(time.Now().Add(time.Minute*20).Unix(), 10), - "jfid": jfId, - "type": "bearer", - } - - tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - token, err := tk.SignedString([]byte(os.Getenv("JFA_SECRET"))) - if err != nil { - return "", "", err - } - claims["exp"] = strconv.FormatInt(time.Now().Add(time.Hour*24).Unix(), 10) - claims["type"] = "refresh" - tk = jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - refresh, err = tk.SignedString([]byte(os.Getenv("JFA_SECRET"))) - if err != nil { - return "", "", err - } - return token, refresh, nil -} - -// Check header for token -func (app *appContext) authenticate(gc *gin.Context) { - header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2) - if header[0] != "Bearer" { - app.debug.Println("Invalid authorization header") - respond(401, "Unauthorized", gc) - return - } - token, err := jwt.Parse(string(header[1]), checkToken) - if err != nil { - app.debug.Printf("Auth denied: %s", err) - respond(401, "Unauthorized", gc) - return - } - claims, ok := token.Claims.(jwt.MapClaims) - expiryUnix, err := strconv.ParseInt(claims["exp"].(string), 10, 64) - if err != nil { - app.debug.Printf("Auth denied: %s", err) - respond(401, "Unauthorized", gc) - return - } - expiry := time.Unix(expiryUnix, 0) - if !(ok && token.Valid && claims["type"].(string) == "bearer" && expiry.After(time.Now())) { - app.debug.Printf("Auth denied: Invalid token") - respond(401, "Unauthorized", gc) - return - } - userID := claims["id"].(string) - jfID := claims["jfid"].(string) - match := false - for _, user := range app.users { - if user.UserID == userID { - match = true - break - } - } - if !match { - app.debug.Printf("Couldn't find user ID \"%s\"", userID) - respond(401, "Unauthorized", gc) - return - } - gc.Set("jfId", jfID) - gc.Set("userId", userID) - app.debug.Println("Auth succeeded") - gc.Next() -} - -func checkToken(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("Unexpected signing method %v", token.Header["alg"]) - } - return []byte(os.Getenv("JFA_SECRET")), nil -} - -type getTokenDTO struct { - Token string `json:"token" example:"kjsdklsfdkljfsjsdfklsdfkldsfjdfskjsdfjklsdf"` // API token for use with everything else. -} - -// @Summary Grabs an API token using username & password. -// @description Click the lock icon next to this, login with your normal jfa-go credentials. Click 'try it out', then 'execute' and an API Key will be returned, copy it (not including quotes). On any of the other routes, click the lock icon and set the API key as "Bearer `your api key`". -// @Produce json -// @Success 200 {object} getTokenDTO -// @Failure 401 {object} stringResponse -// @Router /token/login [get] -// @tags Auth -// @Security getTokenAuth -func (app *appContext) getTokenLogin(gc *gin.Context) { - app.info.Println("Token requested (login attempt)") - header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2) - auth, _ := base64.StdEncoding.DecodeString(header[1]) - creds := strings.SplitN(string(auth), ":", 2) - var userID, jfID string - if creds[0] == "" || creds[1] == "" { - app.debug.Println("Auth denied: blank username/password") - respond(401, "Unauthorized", gc) - return - } - match := false - for _, user := range app.users { - if user.Username == creds[0] && user.Password == creds[1] { - match = true - app.debug.Println("Found existing user") - userID = user.UserID - break - } - } - if !app.jellyfinLogin && !match { - app.info.Println("Auth denied: Invalid username/password") - respond(401, "Unauthorized", gc) - return - } - if !match { - var status int - var err error - var user map[string]interface{} - user, status, err = app.authJf.Authenticate(creds[0], creds[1]) - if status != 200 || err != nil { - if status == 401 || status == 400 { - app.info.Println("Auth denied: Invalid username/password (Jellyfin)") - respond(401, "Unauthorized", gc) - return - } - app.err.Printf("Auth failed: Couldn't authenticate with Jellyfin (%d/%s)", status, err) - respond(500, "Jellyfin error", gc) - return - } - jfID = user["Id"].(string) - if app.config.Section("ui").Key("admin_only").MustBool(true) { - if !user["Policy"].(map[string]interface{})["IsAdministrator"].(bool) { - app.debug.Printf("Auth denied: Users \"%s\" isn't admin", creds[0]) - respond(401, "Unauthorized", gc) - return - } - } - // New users are only added when using jellyfinLogin. - userID = shortuuid.New() - newUser := User{ - UserID: userID, - } - app.debug.Printf("Token generated for user \"%s\"", creds[0]) - app.users = append(app.users, newUser) - } - token, refresh, err := CreateToken(userID, jfID) - if err != nil { - app.err.Printf("getToken failed: Couldn't generate token (%s)", err) - respond(500, "Couldn't generate token", gc) - return - } - gc.SetCookie("refresh", refresh, (3600 * 24), "/", gc.Request.URL.Hostname(), true, true) - gc.JSON(200, getTokenDTO{token}) -} - -// @Summary Grabs an API token using a refresh token from cookies. -// @Produce json -// @Success 200 {object} getTokenDTO -// @Failure 401 {object} stringResponse -// @Router /token/refresh [get] -// @tags Auth -func (app *appContext) getTokenRefresh(gc *gin.Context) { - app.debug.Println("Token requested (refresh token)") - cookie, err := gc.Cookie("refresh") - if err != nil || cookie == "" { - app.debug.Printf("getTokenRefresh denied: Couldn't get token: %s", err) - respond(400, "Couldn't get token", gc) - return - } - for _, token := range app.invalidTokens { - if cookie == token { - app.debug.Println("getTokenRefresh: Invalid token") - respond(401, "Invalid token", gc) - return - } - } - token, err := jwt.Parse(cookie, checkToken) - if err != nil { - app.debug.Println("getTokenRefresh: Invalid token") - respond(400, "Invalid token", gc) - return - } - claims, ok := token.Claims.(jwt.MapClaims) - expiryUnix, err := strconv.ParseInt(claims["exp"].(string), 10, 64) - if err != nil { - app.debug.Printf("getTokenRefresh: Invalid token expiry: %s", err) - respond(401, "Invalid token", gc) - return - } - expiry := time.Unix(expiryUnix, 0) - if !(ok && token.Valid && claims["type"].(string) == "refresh" && expiry.After(time.Now())) { - app.debug.Printf("getTokenRefresh: Invalid token: %s", err) - respond(401, "Invalid token", gc) - return - } - userID := claims["id"].(string) - jfID := claims["jfid"].(string) - jwt, refresh, err := CreateToken(userID, jfID) - if err != nil { - app.err.Printf("getTokenRefresh failed: Couldn't generate token (%s)", err) - respond(500, "Couldn't generate token", gc) - return - } - gc.SetCookie("refresh", refresh, (3600 * 24), "/", gc.Request.URL.Hostname(), true, true) - gc.JSON(200, getTokenDTO{jwt}) -} diff --git a/common/common.go b/common/common.go deleted file mode 100644 index ed94ad1..0000000 --- a/common/common.go +++ /dev/null @@ -1,23 +0,0 @@ -package common - -import ( - "fmt" - "log" -) - -// TimeoutHandler recovers from an http timeout. -type TimeoutHandler func() - -// NewTimeoutHandler returns a new Timeout handler. -func NewTimeoutHandler(name, addr string, noFail bool) TimeoutHandler { - return func() { - if r := recover(); r != nil { - out := fmt.Sprintf("Failed to authenticate with %s @ %s: Timed out", name, addr) - if noFail { - log.Print(out) - } else { - log.Fatalf(out) - } - } - } -} diff --git a/common/go.mod b/common/go.mod deleted file mode 100644 index ec0c50a..0000000 --- a/common/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/hrfee/jfa-go/common - -go 1.15 diff --git a/config.go b/config.go deleted file mode 100644 index 71e1724..0000000 --- a/config.go +++ /dev/null @@ -1,86 +0,0 @@ -package main - -import ( - "fmt" - "path/filepath" - "strconv" - "strings" - - "gopkg.in/ini.v1" -) - -/*var DeCamel ini.NameMapper = func(raw string) string { - out := make([]rune, 0, len(raw)) - upper := 0 - for _, c := range raw { - if unicode.IsUpper(c) { - upper++ - } - if upper == 2 { - out = append(out, '_') - upper = 0 - } - out = append(out, unicode.ToLower(c)) - } - return string(out) -} - -func (app *appContext) loadDefaults() (err error) { - var cfb []byte - cfb, err = ioutil.ReadFile(app.configBase_path) - if err != nil { - return - } - json.Unmarshal(cfb, app.defaults) - return -}*/ - -func (app *appContext) loadConfig() error { - var err error - app.config, err = ini.Load(app.configPath) - if err != nil { - return err - } - - app.config.Section("jellyfin").Key("public_server").SetValue(app.config.Section("jellyfin").Key("public_server").MustString(app.config.Section("jellyfin").Key("server").String())) - - for _, key := range app.config.Section("files").Keys() { - // if key.MustString("") == "" && key.Name() != "custom_css" { - // key.SetValue(filepath.Join(app.data_path, (key.Name() + ".json"))) - // } - if key.Name() != "html_templates" { - key.SetValue(key.MustString(filepath.Join(app.dataPath, (key.Name() + ".json")))) - } - } - for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template"} { - // if app.config.Section("files").Key(key).MustString("") == "" { - // key.SetValue(filepath.Join(app.data_path, (key.Name() + ".json"))) - // } - app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.dataPath, (key + ".json")))) - } - app.URLBase = strings.TrimSuffix(app.config.Section("ui").Key("url_base").MustString(""), "/") - app.config.Section("email").Key("no_username").SetValue(strconv.FormatBool(app.config.Section("email").Key("no_username").MustBool(false))) - - app.config.Section("password_resets").Key("email_html").SetValue(app.config.Section("password_resets").Key("email_html").MustString(filepath.Join(app.localPath, "email.html"))) - app.config.Section("password_resets").Key("email_text").SetValue(app.config.Section("password_resets").Key("email_text").MustString(filepath.Join(app.localPath, "email.txt"))) - - app.config.Section("invite_emails").Key("email_html").SetValue(app.config.Section("invite_emails").Key("email_html").MustString(filepath.Join(app.localPath, "invite-email.html"))) - app.config.Section("invite_emails").Key("email_text").SetValue(app.config.Section("invite_emails").Key("email_text").MustString(filepath.Join(app.localPath, "invite-email.txt"))) - - app.config.Section("notifications").Key("expiry_html").SetValue(app.config.Section("notifications").Key("expiry_html").MustString(filepath.Join(app.localPath, "expired.html"))) - app.config.Section("notifications").Key("expiry_text").SetValue(app.config.Section("notifications").Key("expiry_text").MustString(filepath.Join(app.localPath, "expired.txt"))) - - app.config.Section("notifications").Key("created_html").SetValue(app.config.Section("notifications").Key("created_html").MustString(filepath.Join(app.localPath, "created.html"))) - app.config.Section("notifications").Key("created_text").SetValue(app.config.Section("notifications").Key("created_text").MustString(filepath.Join(app.localPath, "created.txt"))) - - app.config.Section("deletion").Key("email_html").SetValue(app.config.Section("deletion").Key("email_html").MustString(filepath.Join(app.localPath, "deleted.html"))) - app.config.Section("deletion").Key("email_text").SetValue(app.config.Section("deletion").Key("email_text").MustString(filepath.Join(app.localPath, "deleted.txt"))) - - app.config.Section("jellyfin").Key("version").SetValue(VERSION) - app.config.Section("jellyfin").Key("device").SetValue("jfa-go") - app.config.Section("jellyfin").Key("device_id").SetValue(fmt.Sprintf("jfa-go-%s-%s", VERSION, COMMIT)) - - app.email = NewEmailer(app) - - return nil -} diff --git a/config/README.md b/config/README.md deleted file mode 100644 index db64f2b..0000000 --- a/config/README.md +++ /dev/null @@ -1,11 +0,0 @@ -### fixconfig - -Python's `json` library retains the order of data in a JSON file, which meant settings sent to the web page would be in the right order. Go's `encoding/json` and maps do not retain order, so this script opens the json file, and for each section, adds an "order" list which tells the web page in which order to display settings. - -Specify the input and output files with `-i` and `-o` respectively. - -### jsontostruct - -Generates a go struct from `config-base.json`. I wrote this because i was annoyed with the `ini` library, but i've since realised mapping the ini values onto it is painful. - - diff --git a/config/config-base.json b/config/config-base.json deleted file mode 100644 index 21e95a2..0000000 --- a/config/config-base.json +++ /dev/null @@ -1,668 +0,0 @@ -{ - "jellyfin": { - "meta": { - "name": "Jellyfin", - "description": "Settings for connecting to Jellyfin" - }, - "username": { - "name": "Jellyfin Username", - "required": true, - "requires_restart": true, - "type": "text", - "value": "username", - "description": "It is recommended to create a limited admin account for this program." - }, - "password": { - "name": "Jellyfin Password", - "required": true, - "requires_restart": true, - "type": "password", - "value": "password" - }, - "server": { - "name": "Server address", - "required": true, - "requires_restart": true, - "type": "text", - "value": "http://jellyfin.local:8096", - "description": "Jellyfin server address. Can be public, or local for security purposes." - }, - "public_server": { - "name": "Public address", - "required": false, - "requires_restart": false, - "type": "text", - "value": "https://jellyf.in:443", - "description": "Publicly accessible Jellyfin address for invite form. Leave blank to reuse the above address." - }, - "client": { - "name": "Client Name", - "required": true, - "requires_restart": true, - "type": "text", - "value": "jfa-go", - "description": "The name of the client that will show up in the Jellyfin dashboard." - }, - "cache_timeout": { - "name": "User cache timeout (minutes)", - "required": false, - "requires_restart": true, - "type": "number", - "value": 30, - "description": "Timeout of user cache in minutes. Set to 0 to disable." - } - }, - "ui": { - "meta": { - "name": "General", - "description": "Settings related to the UI and program functionality." - }, - "language": { - "name": "Language", - "required": false, - "requires_restart": true, - "type": "select", - "options": [ - "en-us" - ], - "value": "en-US", - "description": "UI Language. Currently only implemented for account creation form. Submit a PR on github if you'd like to translate." - }, - "theme": { - "name": "Default Look", - "required": false, - "requires_restart": true, - "type": "select", - "options": [ - "Bootstrap (Light)", - "Jellyfin (Dark)", - "Custom CSS" - ], - "value": "Jellyfin (Dark)", - "description": "Default appearance for all users." - }, - "host": { - "name": "Address", - "required": true, - "requires_restart": true, - "type": "text", - "value": "0.0.0.0", - "description": "Set 0.0.0.0 to run on localhost" - }, - "port": { - "name": "Port", - "required": true, - "requires_restart": true, - "type": "number", - "value": 8056 - }, - "jellyfin_login": { - "name": "Use Jellyfin for authentication", - "required": false, - "requires_restart": true, - "type": "bool", - "value": true, - "description": "Enable this to use Jellyfin users instead of the below username and pw." - }, - "admin_only": { - "name": "Allow admin users only", - "required": false, - "requires_restart": true, - "depends_true": "jellyfin_login", - "type": "bool", - "value": true, - "description": "Allows only admin users on Jellyfin to access the admin page." - }, - "username": { - "name": "Web Username", - "required": true, - "requires_restart": true, - "depends_false": "jellyfin_login", - "type": "text", - "value": "your username", - "description": "Username for admin page (Leave blank if using jellyfin_login)" - }, - "password": { - "name": "Web Password", - "required": true, - "requires_restart": true, - "depends_false": "jellyfin_login", - "type": "password", - "value": "your password", - "description": "Password for admin page (Leave blank if using jellyfin_login)" - }, - "email": { - "name": "Admin email address", - "required": false, - "requires_restart": false, - "depends_false": "jellyfin_login", - "type": "text", - "value": "example@example.com", - "description": "Address to send notifications to (Leave blank if using jellyfin_login)" - }, - "debug": { - "name": "Debug logging", - "required": false, - "requires_restart": true, - "type": "bool", - "value": false, - "description": "Enables debug logging and exposes pprof as a route (Don't use in production!)" - }, - "contact_message": { - "name": "Contact message", - "required": false, - "requires_restart": false, - "type": "text", - "value": "Need help? contact me.", - "description": "Displayed at bottom of all pages except admin" - }, - "help_message": { - "name": "Help message", - "required": false, - "requires_restart": false, - "type": "text", - "value": "Enter your details to create an account.", - "description": "Displayed at top of invite form." - }, - "success_message": { - "name": "Success message", - "required": false, - "requires_restart": false, - "type": "text", - "value": "Your account has been created. Click below to continue to Jellyfin.", - "description": "Displayed when a user creates an account" - }, - "bs5": { - "name": "Use Bootstrap 5", - "required": false, - "requires_restart": true, - "type": "bool", - "value": false, - "description": "Use the Bootstrap 5 Alpha. Looks better and removes the need for jQuery, so the page should load faster." - }, - "url_base": { - "name": "URL Base", - "required": false, - "requires_restart": true, - "type": "text", - "value": "", - "description": "URL base for when running jfa-go with a reverse proxy in a subfolder." - } - }, - "password_validation": { - "meta": { - "name": "Password Validation", - "description": "Password validation (minimum length, etc.)" - }, - "enabled": { - "name": "Enabled", - "required": false, - "requires_restart": false, - "type": "bool", - "value": true - }, - "min_length": { - "name": "Minimum Length", - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "8" - }, - "upper": { - "name": "Minimum uppercase characters", - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "1" - }, - "lower": { - "name": "Minimum lowercase characters", - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "0" - }, - "number": { - "name": "Minimum number count", - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "1" - }, - "special": { - "name": "Minimum number of special characters", - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "0" - } - }, - "email": { - "meta": { - "name": "Email", - "description": "General email settings. Ignore if not using email features." - }, - "no_username": { - "name": "Use email addresses as username", - "required": false, - "requires_restart": false, - "depends_true": "method", - "type": "bool", - "value": false, - "description": "Use email address from invite form as username on Jellyfin." - }, - "use_24h": { - "name": "Use 24h time", - "required": false, - "requires_restart": false, - "depends_true": "method", - "type": "bool", - "value": true - }, - "date_format": { - "name": "Date format", - "required": false, - "requires_restart": false, - "depends_true": "method", - "type": "text", - "value": "%d/%m/%y", - "description": "Date format used in emails. Follows datetime.strftime format." - }, - "message": { - "name": "Help message", - "required": false, - "requires_restart": false, - "depends_true": "method", - "type": "text", - "value": "Need help? contact me.", - "description": "Message displayed at bottom of emails." - }, - "method": { - "name": "Email method", - "required": false, - "requires_restart": false, - "type": "select", - "options": [ - "smtp", - "mailgun" - ], - "value": "smtp", - "description": "Method of sending email to use." - }, - "address": { - "name": "Sent from (address)", - "required": false, - "requires_restart": false, - "depends_true": "method", - "type": "email", - "value": "jellyfin@jellyf.in", - "description": "Address to send emails from" - }, - "from": { - "name": "Sent from (name)", - "required": false, - "requires_restart": false, - "depends_true": "method", - "type": "text", - "value": "Jellyfin", - "description": "The name of the sender" - } - }, - "password_resets": { - "meta": { - "name": "Password Resets", - "description": "Settings for the password reset handler." - }, - "enabled": { - "name": "Enabled", - "required": false, - "requires_restart": true, - "type": "bool", - "value": true, - "description": "Enable to store provided email addresses, monitor Jellyfin directory for pw-resets, and send reset pins" - }, - "watch_directory": { - "name": "Jellyfin directory", - "required": false, - "requires_restart": true, - "depends_true": "enabled", - "type": "text", - "value": "/path/to/jellyfin", - "description": "Path to the folder Jellyfin puts password-reset files." - }, - "email_html": { - "name": "Custom email (HTML)", - "required": false, - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "", - "description": "Path to custom email html" - }, - "email_text": { - "name": "Custom email (plaintext)", - "required": false, - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "", - "description": "Path to custom email in plain text" - }, - "subject": { - "name": "Email subject", - "required": false, - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "Password Reset - Jellyfin", - "description": "Subject of password reset emails." - } - }, - "invite_emails": { - "meta": { - "name": "Invite emails", - "description": "Settings for sending invites directly to users." - }, - "enabled": { - "name": "Enabled", - "required": false, - "requires_restart": false, - "type": "bool", - "value": true - }, - "email_html": { - "name": "Custom email (HTML)", - "required": false, - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "", - "description": "Path to custom email HTML" - }, - "email_text": { - "name": "Custom email (plaintext)", - "required": false, - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "", - "description": "Path to custom email in plain text" - }, - "subject": { - "name": "Email subject", - "required": true, - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "Invite - Jellyfin", - "description": "Subject of invite emails." - }, - "url_base": { - "name": "URL Base", - "required": true, - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "http://accounts.jellyf.in:8056/invite", - "description": "Base URL for jfa-go. This is necessary because using a reverse proxy means the program has no way of knowing the URL itself." - } - }, - "notifications": { - "meta": { - "name": "Notifications", - "description": "Notification related settings." - }, - "enabled": { - "name": "Enabled", - "required": "false", - "requires_restart": true, - "type": "bool", - "value": true, - "description": "Enabling adds optional toggles to invites to notify on expiry and user creation." - }, - "expiry_html": { - "name": "Expiry email (HTML)", - "required": false, - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "", - "description": "Path to expiry notification email HTML." - }, - "expiry_text": { - "name": "Expiry email (Plaintext)", - "required": false, - "requires_restart": "false", - "depends_true": "enabled", - "type": "text", - "value": "", - "description": "Path to expiry notification email in plaintext." - }, - "created_html": { - "name": "User created email (HTML)", - "required": false, - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "", - "description": "Path to user creation notification email HTML." - }, - "created_text": { - "name": "User created email (Plaintext)", - "required": false, - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "", - "description": "Path to user creation notification email in plaintext." - } - }, - "mailgun": { - "meta": { - "name": "Mailgun (Email)", - "description": "Mailgun API connection settings" - }, - "api_url": { - "name": "API URL", - "required": false, - "requires_restart": false, - "type": "text", - "value": "https://api.mailgun.net..." - }, - "api_key": { - "name": "API Key", - "required": false, - "requires_restart": false, - "type": "text", - "value": "your api key" - } - }, - "smtp": { - "meta": { - "name": "SMTP (Email)", - "description": "SMTP Server connection settings." - }, - "username": { - "name": "Username", - "required": false, - "requires_restart": false, - "type": "text", - "value": "", - "description": "Username for SMTP. Leave blank to user send from address as username." - }, - "encryption": { - "name": "Encryption Method", - "required": false, - "requires_restart": false, - "type": "select", - "options": [ - "ssl_tls", - "starttls" - ], - "value": "starttls", - "description": "Your email provider should provide different ports for each encryption method. Generally 465 for ssl_tls, 587 for starttls." - }, - "server": { - "name": "Server address", - "required": false, - "requires_restart": false, - "type": "text", - "value": "smtp.jellyf.in", - "description": "SMTP Server address." - }, - "port": { - "name": "Port", - "required": false, - "requires_restart": false, - "type": "number", - "value": 465 - }, - "password": { - "name": "Password", - "required": false, - "requires_restart": false, - "type": "password", - "value": "smtp password" - } - }, - "ombi": { - "meta": { - "name": "Ombi Integration", - "description": "Connect to Ombi to automatically create both Ombi and Jellyfin accounts for new users. You'll need to create a user template for this to work. Once enabled, refresh to see an option in settings for this." - }, - "enabled": { - "name": "Enabled", - "required": false, - "requires_restart": true, - "type": "bool", - "value": false, - "description": "Enable to create an Ombi account for new Jellyfin users" - }, - "server": { - "name": "URL", - "required": false, - "requires_restart": true, - "type": "text", - "value": "localhost:5000", - "depends_true": "enabled", - "description": "Ombi server URL, including http(s)://." - }, - "api_key": { - "name": "API Key", - "required": false, - "requires_restart": true, - "type": "text", - "value": "", - "depends_true": "enabled", - "description": "API Key. Get this from the first tab in Ombi settings." - } - }, - "deletion": { - "meta": { - "name": "Account Deletion", - "description": "Subject/email files for account deletion emails." - }, - "subject": { - "name": "Email subject", - "required": false, - "requires_restart": false, - "type": "text", - "value": "Your account was deleted - Jellyfin", - "description": "Subject of account deletion emails." - }, - "email_html": { - "name": "Custom email (HTML)", - "required": false, - "requires_restart": false, - "type": "text", - "value": "", - "description": "Path to custom email html" - }, - "email_text": { - "name": "Custom email (plaintext)", - "required": false, - "requires_restart": false, - "type": "text", - "value": "", - "description": "Path to custom email in plain text" - } - }, - "files": { - "meta": { - "name": "File Storage", - "description": "Optional settings for changing storage locations." - }, - "invites": { - "name": "Invite Storage", - "required": false, - "requires_restart": true, - "type": "text", - "value": "", - "description": "Location of stored invites (json)." - }, - "emails": { - "name": "Email Addresses", - "required": false, - "requires_restart": true, - "type": "text", - "value": "", - "description": "Location of stored email addresses (json)." - }, - "ombi_template": { - "name": "Ombi user template", - "required": false, - "requires_restart": false, - "type": "text", - "value": "", - "description": "Location of stored Ombi user template." - }, - "user_template": { - "name": "User Template (Deprecated)", - "required": false, - "requires_restart": true, - "type": "text", - "value": "", - "description": "Deprecated in favor of User Profiles. Location of stored user policy template (json)." - }, - "user_configuration": { - "name": "userConfiguration (Deprecated)", - "required": false, - "requires_restart": true, - "type": "text", - "value": "", - "description": "Deprecated in favor of User Profiles. Location of stored user configuration template (used for setting homescreen layout) (json)" - }, - "user_displayprefs": { - "name": "displayPreferences (Deprecated)", - "required": false, - "requires_restart": true, - "type": "text", - "value": "", - "description": "Deprecated in favor of User Profiles. Location of stored displayPreferences template (also used for homescreen layout) (json)" - }, - "user_profiles": { - "name": "User Profiles", - "required": false, - "requires_restart": true, - "type": "text", - "value": "", - "description": "Location of stored user profiles (encompasses template and configuration and displayprefs) (json)" - }, - "custom_css": { - "name": "Custom CSS", - "required": false, - "requires_restart": true, - "type": "text", - "value": "", - "description": "Location of custom bootstrap CSS." - }, - "html_templates": { - "name": "Custom HTML Template Directory", - "required": false, - "requires_restart": true, - "type": "text", - "value": "", - "description": "Path to directory containing custom versions of web ui pages. See wiki for more info." - } - } -} diff --git a/config/configStruct.go b/config/configStruct.go deleted file mode 100644 index a58b2d9..0000000 --- a/config/configStruct.go +++ /dev/null @@ -1,541 +0,0 @@ -package main - -type Metadata struct{ - Name string `json:"name"` - Description string `json:"description"` -} - -type Config struct{ - Order []string `json:"order"` - Jellyfin struct{ - Order []string `json:"order"` - Meta Metadata `json:"meta"` - Username struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"username"` - } `json:"username" cfg:"username"` - Password struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"password"` - } `json:"password" cfg:"password"` - Server struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"server"` - } `json:"server" cfg:"server"` - PublicServer struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"public_server"` - } `json:"public_server" cfg:"public_server"` - Client struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"client"` - } `json:"client" cfg:"client"` - Version struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"version"` - } `json:"version" cfg:"version"` - Device struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"device"` - } `json:"device" cfg:"device"` - DeviceId struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"device_id"` - } `json:"device_id" cfg:"device_id"` - } `json:"jellyfin"` - Ui struct{ - Order []string `json:"order"` - Meta Metadata `json:"meta"` - Theme struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Options []string `json:"options"` - Value string `json:"value" cfg:"theme"` - } `json:"theme" cfg:"theme"` - Host struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"host"` - } `json:"host" cfg:"host"` - Port struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value int `json:"value" cfg:"port"` - } `json:"port" cfg:"port"` - JellyfinLogin struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value bool `json:"value" cfg:"jellyfin_login"` - } `json:"jellyfin_login" cfg:"jellyfin_login"` - AdminOnly struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value bool `json:"value" cfg:"admin_only"` - } `json:"admin_only" cfg:"admin_only"` - Username struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"username"` - } `json:"username" cfg:"username"` - Password struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"password"` - } `json:"password" cfg:"password"` - Email struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"email"` - } `json:"email" cfg:"email"` - Debug struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value bool `json:"value" cfg:"debug"` - } `json:"debug" cfg:"debug"` - ContactMessage struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"contact_message"` - } `json:"contact_message" cfg:"contact_message"` - HelpMessage struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"help_message"` - } `json:"help_message" cfg:"help_message"` - SuccessMessage struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"success_message"` - } `json:"success_message" cfg:"success_message"` - Bs5 struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value bool `json:"value" cfg:"bs5"` - } `json:"bs5" cfg:"bs5"` - } `json:"ui"` - PasswordValidation struct{ - Order []string `json:"order"` - Meta Metadata `json:"meta"` - Enabled struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value bool `json:"value" cfg:"enabled"` - } `json:"enabled" cfg:"enabled"` - MinLength struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"min_length"` - } `json:"min_length" cfg:"min_length"` - Upper struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"upper"` - } `json:"upper" cfg:"upper"` - Lower struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"lower"` - } `json:"lower" cfg:"lower"` - Number struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"number"` - } `json:"number" cfg:"number"` - Special struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"special"` - } `json:"special" cfg:"special"` - } `json:"password_validation"` - Email struct{ - Order []string `json:"order"` - Meta Metadata `json:"meta"` - NoUsername struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value bool `json:"value" cfg:"no_username"` - } `json:"no_username" cfg:"no_username"` - Use24H struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value bool `json:"value" cfg:"use_24h"` - } `json:"use_24h" cfg:"use_24h"` - DateFormat struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"date_format"` - } `json:"date_format" cfg:"date_format"` - Message struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"message"` - } `json:"message" cfg:"message"` - Method struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Options []string `json:"options"` - Value string `json:"value" cfg:"method"` - } `json:"method" cfg:"method"` - Address struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"address"` - } `json:"address" cfg:"address"` - From struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"from"` - } `json:"from" cfg:"from"` - } `json:"email"` - PasswordResets struct{ - Order []string `json:"order"` - Meta Metadata `json:"meta"` - Enabled struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value bool `json:"value" cfg:"enabled"` - } `json:"enabled" cfg:"enabled"` - WatchDirectory struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"watch_directory"` - } `json:"watch_directory" cfg:"watch_directory"` - EmailHtml struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"email_html"` - } `json:"email_html" cfg:"email_html"` - EmailText struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"email_text"` - } `json:"email_text" cfg:"email_text"` - Subject struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"subject"` - } `json:"subject" cfg:"subject"` - } `json:"password_resets"` - InviteEmails struct{ - Order []string `json:"order"` - Meta Metadata `json:"meta"` - Enabled struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value bool `json:"value" cfg:"enabled"` - } `json:"enabled" cfg:"enabled"` - EmailHtml struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"email_html"` - } `json:"email_html" cfg:"email_html"` - EmailText struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"email_text"` - } `json:"email_text" cfg:"email_text"` - Subject struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"subject"` - } `json:"subject" cfg:"subject"` - UrlBase struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"url_base"` - } `json:"url_base" cfg:"url_base"` - } `json:"invite_emails"` - Notifications struct{ - Order []string `json:"order"` - Meta Metadata `json:"meta"` - Enabled struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value bool `json:"value" cfg:"enabled"` - } `json:"enabled" cfg:"enabled"` - ExpiryHtml struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"expiry_html"` - } `json:"expiry_html" cfg:"expiry_html"` - ExpiryText struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"expiry_text"` - } `json:"expiry_text" cfg:"expiry_text"` - CreatedHtml struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"created_html"` - } `json:"created_html" cfg:"created_html"` - CreatedText struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"created_text"` - } `json:"created_text" cfg:"created_text"` - } `json:"notifications"` - Mailgun struct{ - Order []string `json:"order"` - Meta Metadata `json:"meta"` - ApiUrl struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"api_url"` - } `json:"api_url" cfg:"api_url"` - ApiKey struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"api_key"` - } `json:"api_key" cfg:"api_key"` - } `json:"mailgun"` - Smtp struct{ - Order []string `json:"order"` - Meta Metadata `json:"meta"` - Encryption struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Options []string `json:"options"` - Value string `json:"value" cfg:"encryption"` - } `json:"encryption" cfg:"encryption"` - Server struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"server"` - } `json:"server" cfg:"server"` - Port struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value int `json:"value" cfg:"port"` - } `json:"port" cfg:"port"` - Password struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"password"` - } `json:"password" cfg:"password"` - } `json:"smtp"` - Files struct{ - Order []string `json:"order"` - Meta Metadata `json:"meta"` - Invites struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"invites"` - } `json:"invites" cfg:"invites"` - Emails struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"emails"` - } `json:"emails" cfg:"emails"` - UserTemplate struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"user_template"` - } `json:"user_template" cfg:"user_template"` - UserConfiguration struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"user_configuration"` - } `json:"user_configuration" cfg:"user_configuration"` - UserDisplayprefs struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"user_displayprefs"` - } `json:"user_displayprefs" cfg:"user_displayprefs"` - CustomCss struct{ - Name string `json:"name"` - Required bool `json:"required"` - Restart bool `json:"requires_restart"` - Description string `json:"description"` - Type string `json:"type"` - Value string `json:"value" cfg:"custom_css"` - } `json:"custom_css" cfg:"custom_css"` - } `json:"files"` -} diff --git a/config/fixconfig.py b/config/fixconfig.py deleted file mode 100644 index 180f2e4..0000000 --- a/config/fixconfig.py +++ /dev/null @@ -1,27 +0,0 @@ -import json, argparse - -parser = argparse.ArgumentParser() -parser.add_argument("-i", "--input", help="input config base from jf-accounts") -parser.add_argument("-o", "--output", help="output config base for jfa-go") - -args = parser.parse_args() - -with open(args.input, 'r') as f: - config = json.load(f) - -newconfig = {"order": []} - -for sect in config: - newconfig["order"].append(sect) - newconfig[sect] = {} - newconfig[sect]["order"] = [] - newconfig[sect]["meta"] = config[sect]["meta"] - for setting in config[sect]: - if setting != "meta": - newconfig[sect]["order"].append(setting) - newconfig[sect][setting] = config[sect][setting] - -with open(args.output, 'w') as f: - f.write(json.dumps(newconfig, indent=4)) - - diff --git a/config/generate_ini.py b/config/generate_ini.py deleted file mode 100644 index efc8c0a..0000000 --- a/config/generate_ini.py +++ /dev/null @@ -1,42 +0,0 @@ -# Generates config file -import configparser -import json -import argparse -from pathlib import Path - - -def generate_ini(base_file, ini_file): - """ - Generates .ini file from config-base file. - """ - with open(Path(base_file), "r") as f: - config_base = json.load(f) - - ini = configparser.RawConfigParser(allow_no_value=True) - - for section in config_base: - ini.add_section(section) - for entry in config_base[section]: - if "description" in config_base[section][entry]: - ini.set(section, "; " + config_base[section][entry]["description"]) - if entry != "meta": - value = config_base[section][entry]["value"] - if isinstance(value, bool): - value = str(value).lower() - else: - value = str(value) - ini.set(section, entry, value) - - with open(Path(ini_file), "w") as config_file: - ini.write(config_file) - return True - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("-i", "--input", help="input config base from jf-accounts") - parser.add_argument("-o", "--output", help="output ini") - - args = parser.parse_args() - - print(generate_ini(base_file=args.input, ini_file=args.output)) diff --git a/config/jsontostruct.py b/config/jsontostruct.py deleted file mode 100644 index b99be2b..0000000 --- a/config/jsontostruct.py +++ /dev/null @@ -1,57 +0,0 @@ -import json - -with open("config-formatted.json", "r") as f: - config = json.load(f) - -indent = 0 - - -def writeln(ln): - global indent - if "}" in ln and "{" not in ln: - indent -= 1 - s.write(("\t" * indent) + ln + "\n") - if "{" in ln and "}" not in ln: - indent += 1 - - -with open("configStruct.go", "w") as s: - writeln("package main") - writeln("") - writeln("type Metadata struct{") - writeln('Name string `json:"name"`') - writeln('Description string `json:"description"`') - writeln("}") - writeln("") - writeln("type Config struct{") - if "order" in config: - writeln('Order []string `json:"order"`') - for section in [x for x in config.keys() if x != "order"]: - title = "".join([x.title() for x in section.split("_")]) - writeln(title + " struct{") - if "order" in config[section]: - writeln('Order []string `json:"order"`') - if "meta" in config[section]: - writeln('Meta Metadata `json:"meta"`') - for setting in [ - x for x in config[section].keys() if x != "order" and x != "meta" - ]: - name = "".join([x.title() for x in setting.split("_")]) - writeln(name + " struct{") - writeln('Name string `json:"name"`') - writeln('Required bool `json:"required"`') - writeln('Restart bool `json:"requires_restart"`') - writeln('Description string `json:"description"`') - writeln('Type string `json:"type"`') - dt = config[section][setting]["type"] - if dt == "select": - dt = "string" - writeln('Options []string `json:"options"`') - elif dt == "number": - dt = "int" - elif dt != "bool": - dt = "string" - writeln(f'Value {dt} `json:"value" cfg:"{setting}"`') - writeln("} " + f'`json:"{setting}" cfg:"{setting}"`') - writeln("} " + f'`json:"{section}"`') - writeln("}") diff --git a/daemon.go b/daemon.go deleted file mode 100644 index 51299dc..0000000 --- a/daemon.go +++ /dev/null @@ -1,50 +0,0 @@ -package main - -import "time" - -// https://bbengfort.github.io/snippets/2016/06/26/background-work-goroutines-timer.html THANKS - -type repeater struct { - Stopped bool - ShutdownChannel chan string - Interval time.Duration - period time.Duration - app *appContext -} - -func newRepeater(interval time.Duration, app *appContext) *repeater { - return &repeater{ - Stopped: false, - ShutdownChannel: make(chan string), - Interval: interval, - period: interval, - app: app, - } -} - -func (rt *repeater) run() { - rt.app.info.Println("Invite daemon started") - for { - select { - case <-rt.ShutdownChannel: - rt.ShutdownChannel <- "Down" - return - case <-time.After(rt.period): - break - } - started := time.Now() - rt.app.storage.loadInvites() - rt.app.debug.Println("Daemon: Checking invites") - rt.app.checkInvites() - finished := time.Now() - duration := finished.Sub(started) - rt.period = rt.Interval - duration - } -} - -func (rt *repeater) shutdown() { - rt.Stopped = true - rt.ShutdownChannel <- "Down" - <-rt.ShutdownChannel - close(rt.ShutdownChannel) -} diff --git a/data/lang/form/en-us.json b/data/lang/form/en-us.json deleted file mode 100644 index fe94b3f..0000000 --- a/data/lang/form/en-us.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "meta": { - "name": "English (US)" - }, - "strings": { - "pageTitle": "Create Jellyfin Account", - "createAccountHeader": "Create Account", - "accountDetails": "Details", - "emailAddress": "Email", - "username": "Username", - "password": "Password", - "reEnterPassword": "Re-enter Password", - "reEnterPasswordInvalid": "Passwords are not the same.", - "createAccountButton": "Create Account", - "passwordRequirementsHeader": "Password Requirements", - "successHeader": "Success!", - "successContinueButton": "Continue", - "validationStrings": { - "length": { - "singular": "Must have at least {n} character", - "plural": "Must have a least {n} characters" - }, - "uppercase": { - "singular": "Must have at least {n} uppercase character", - "plural": "Must have at least {n} uppercase characters" - }, - "lowercase": { - "singular": "Must have at least {n} lowercase character", - "plural": "Must have at least {n} lowercase characters" - }, - "number": { - "singular": "Must have at least {n} number", - "plural": "Must have at least {n} numbers" - }, - "special": { - "singular": "Must have at least {n} special character", - "plural": "Must have at least {n} special characters" - } - } - } -} diff --git a/data/lang/form/fr-fr.json b/data/lang/form/fr-fr.json deleted file mode 100644 index 102148e..0000000 --- a/data/lang/form/fr-fr.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "meta": { - "name": "Francais (FR)", - "author": "https://github.com/Killianbe" - }, - "strings": { - "pageTitle": "Créer un compte Jellyfin", - "createAccountHeader": "Création du compte", - "accountDetails": "Détails", - "emailAddress": "Email", - "username": "Pseudo", - "password": "Mot de passe", - "reEnterPassword": "Confirmez mot de passe", - "reEnterPasswordInvalid": "Les mots de passe ne correspondent pas.", - "createAccountButton": "Créer le compte", - "passwordRequirementsHeader": "Mot de passe requis", - "successHeader": "Succes!", - "successContinueButton": "Continuer", - "validationStrings": { - "length": { - "singular": "Doit avoir au moins {n} caractère", - "plural": "Doit avoir au moins {n} caractères" - }, - "uppercase": { - "singular": "Doit avoir au moins {n} caractère majuscule", - "plural": "Must have at least {n} caractères majuscules" - }, - "lowercase": { - "singular": "Doit avoir au moins {n} caractère minuscule", - "plural": "Doit avoir au moins {n} caractères minuscules" - }, - "number": { - "singular": "Doit avoir au moins {n} nombre", - "plural": "Doit avoir au moins {n} nombres" - }, - "special": { - "singular": "Doit avoir au moins {n} caractère spécial", - "plural": "Doit avoir au moins {n} caractères spéciaux" - } - } - } -} diff --git a/data/static/android-chrome-192x192.png b/data/static/android-chrome-192x192.png deleted file mode 100644 index 43660ed..0000000 Binary files a/data/static/android-chrome-192x192.png and /dev/null differ diff --git a/data/static/android-chrome-512x512.png b/data/static/android-chrome-512x512.png deleted file mode 100644 index 0eed6a0..0000000 Binary files a/data/static/android-chrome-512x512.png and /dev/null differ diff --git a/data/static/apple-touch-icon.png b/data/static/apple-touch-icon.png deleted file mode 100644 index 6f88cbf..0000000 Binary files a/data/static/apple-touch-icon.png and /dev/null differ diff --git a/data/static/banner.svg b/data/static/banner.svg deleted file mode 100644 index 627a830..0000000 --- a/data/static/banner.svg +++ /dev/null @@ -1,283 +0,0 @@ - - diff --git a/data/static/browserconfig.xml b/data/static/browserconfig.xml deleted file mode 100644 index 5cd27e3..0000000 --- a/data/static/browserconfig.xml +++ /dev/null @@ -1,9 +0,0 @@ - -- {{ else }} - | - {{ end }} - | Username | -Email Address | -Last Active | -
---|
Note: * Indicates required field, R Indicates changes require a restart.
- - - {{ if .ombiEnabled }} - - {{ end }} -Profiles are applied to users when they create an account. They include things like access rights and homescreen layout. You can create them here.
-Name | -Default | -From | -Admin? | -Libraries | -- |
---|
{{ .contactMessage }}
-{{ .helpMessage }}
-{{ .contactMessage }}
-A user was created using code {{ .code }}.
-