diff --git a/api.go b/api.go index d944534..251a990 100644 --- a/api.go +++ b/api.go @@ -3,8 +3,9 @@ package main import ( "fmt" "github.com/gin-gonic/gin" - "github.com/google/uuid" "github.com/knz/strtime" + "github.com/lithammer/shortuuid/v3" + "gopkg.in/ini.v1" "time" ) @@ -82,25 +83,24 @@ func (ctx *appContext) checkInvite(code string, used bool, username string) bool changed := false for invCode, data := range ctx.storage.invites { expiry := data.ValidTill - fmt.Println("Expiry:", expiry) if current_time.After(expiry) { - // NOTIFICATIONS + ctx.debug.Printf("Housekeeping: Deleting old invite %s", code) notify := data.Notify if ctx.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 { + ctx.debug.Printf("%s: Expiry notification", code) for address, settings := range notify { if settings["notify-expiry"] { if ctx.email.constructExpiry(invCode, data, ctx) != nil { - fmt.Println("failed expiry construct") + ctx.err.Printf("%s: Failed to construct expiry notification", code) + } else if ctx.email.send(address, ctx) != nil { + ctx.err.Printf("%s: Failed to send expiry notification", code) } else { - if ctx.email.send(address, ctx) != nil { - fmt.Println("failed expiry send") - } + ctx.info.Printf("Sent expiry notification to %s", address) } } } } changed = true - fmt.Println("Deleting:", invCode) delete(ctx.storage.invites, invCode) } else if invCode == code { match = true @@ -130,7 +130,6 @@ func (ctx *appContext) checkInvite(code string, used bool, username string) bool // Routes from now on! -// POST type newUserReq struct { Username string `json:"username"` Password string `json:"password"` @@ -141,7 +140,9 @@ type newUserReq struct { func (ctx *appContext) NewUser(gc *gin.Context) { var req newUserReq gc.BindJSON(&req) + ctx.debug.Printf("%s: New user attempt", req.Code) if !ctx.checkInvite(req.Code, false, "") { + ctx.info.Printf("%s New user failed: invalid code", req.Code) gc.JSON(401, map[string]bool{"success": false}) gc.Abort() return @@ -155,18 +156,21 @@ func (ctx *appContext) NewUser(gc *gin.Context) { } if !valid { // 200 bcs idk what i did in js - fmt.Println("invalid") + ctx.info.Printf("%s New user failed: Invalid password", req.Code) gc.JSON(200, validation) gc.Abort() return } existingUser, _, _ := ctx.jf.userByName(req.Username, false) if existingUser != nil { - respond(401, fmt.Sprintf("User already exists named %s", req.Username), gc) + msg := fmt.Sprintf("User already exists named %s", req.Username) + ctx.info.Printf("%s New user failed: %s", req.Code, msg) + respond(401, msg, gc) return } user, status, err := ctx.jf.newUser(req.Username, req.Password) if !(status == 200 || status == 204) || err != nil { + ctx.err.Printf("%s New user failed: Jellyfin responded with %d", req.Code, status) respond(401, "Unknown error", gc) return } @@ -176,9 +180,13 @@ func (ctx *appContext) NewUser(gc *gin.Context) { for address, settings := range invite.Notify { if settings["notify-creation"] { if ctx.email.constructCreated(req.Code, req.Username, req.Email, invite, ctx) != nil { - fmt.Println("created template failed") + ctx.err.Printf("%s: Failed to construct user creation notification", req.Code) + ctx.debug.Printf("%s: Error: %s", req.Code, err) } else if ctx.email.send(address, ctx) != nil { - fmt.Println("created send failed") + ctx.err.Printf("%s: Failed to send user creation notification", req.Code) + ctx.debug.Printf("%s: Error: %s", req.Code, err) + } else { + ctx.info.Printf("%s: Sent user creation notification to %s", req.Code, address) } } } @@ -190,13 +198,15 @@ func (ctx *appContext) NewUser(gc *gin.Context) { if len(ctx.storage.policy) != 0 { status, err = ctx.jf.setPolicy(id, ctx.storage.policy) if !(status == 200 || status == 204) { - fmt.Printf("Failed to set user policy") + ctx.err.Printf("%s: Failed to set user policy: Code %d", req.Code, status) } } if len(ctx.storage.configuration) != 0 && len(ctx.storage.displayprefs) != 0 { status, err = ctx.jf.setConfiguration(id, ctx.storage.configuration) - if (status == 200 || status == 204) && err != nil { + if (status == 200 || status == 204) && err == nil { status, err = ctx.jf.setDisplayPreferences(id, ctx.storage.displayprefs) + } else { + ctx.err.Printf("%s: Failed to set configuration template: Code %d", req.Code, status) } } if ctx.config.Section("password_resets").Key("enabled").MustBool(false) { @@ -213,18 +223,18 @@ type generateInviteReq struct { Email string `json:"email"` MultipleUses bool `json:"multiple-uses"` NoLimit bool `json:"no-limit"` - RemainingUses int `json:remaining-uses"` + RemainingUses int `json:"remaining-uses"` } func (ctx *appContext) GenerateInvite(gc *gin.Context) { var req generateInviteReq + ctx.debug.Println("Generating new invite") ctx.storage.loadInvites() gc.BindJSON(&req) current_time := time.Now() - fmt.Println(req.Days, req.Hours, req.Minutes) valid_till := current_time.AddDate(0, 0, req.Days) valid_till = valid_till.Add(time.Hour*time.Duration(req.Hours) + time.Minute*time.Duration(req.Minutes)) - invite_code, _ := uuid.NewRandom() + invite_code := shortuuid.New() var invite Invite invite.Created = current_time if req.MultipleUses { @@ -238,22 +248,27 @@ func (ctx *appContext) GenerateInvite(gc *gin.Context) { } invite.ValidTill = valid_till if req.Email != "" && ctx.config.Section("invite_emails").Key("enabled").MustBool(false) { + ctx.debug.Printf("%s: Sending invite email", invite_code) invite.Email = req.Email - if err := ctx.email.constructInvite(invite_code.String(), invite, ctx); err != nil { - fmt.Println("error sending:", err) + if err := ctx.email.constructInvite(invite_code, invite, ctx); err != nil { invite.Email = fmt.Sprintf("Failed to send to %s", req.Email) + ctx.err.Printf("%s: Failed to construct invite email", invite_code) + ctx.debug.Printf("%s: Error: %s", invite_code, err) } else if err := ctx.email.send(req.Email, ctx); err != nil { - fmt.Println("error sending:", err) invite.Email = fmt.Sprintf("Failed to send to %s", req.Email) + ctx.err.Printf("%s: %s", invite_code, invite.Email) + ctx.debug.Printf("%s: Error: %s", invite_code, err) + } else { + ctx.info.Printf("%s: Sent invite email to %s", invite_code, req.Email) } } - ctx.storage.invites[invite_code.String()] = invite - fmt.Println("INVITES FROM API:", ctx.storage.invites) + ctx.storage.invites[invite_code] = invite ctx.storage.storeInvites() - fmt.Println("New inv") gc.JSON(200, map[string]bool{"success": true}) } +// logged up to here! + func (ctx *appContext) GetInvites(gc *gin.Context) { current_time := time.Now() // checking one checks all of them @@ -494,3 +509,20 @@ func (ctx *appContext) GetConfig(gc *gin.Context) { } gc.JSON(200, resp) } + +func (ctx *appContext) ModifyConfig(gc *gin.Context) { + var req map[string]interface{} + gc.BindJSON(&req) + tempConfig, _ := ini.Load(ctx.config_path) + for section, settings := range req { + _, err := tempConfig.GetSection(section) + if section != "restart-program" && err == nil { + for setting, value := range settings.(map[string]interface{}) { + tempConfig.Section(section).Key(setting).SetValue(value.(string)) + } + } + } + tempConfig.SaveTo(ctx.config_path) + gc.JSON(200, map[string]bool{"success": true}) + ctx.loadConfig() +} diff --git a/auth.go b/auth.go index 02acc6b..2efd41d 100644 --- a/auth.go +++ b/auth.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/dgrijalva/jwt-go" "github.com/gin-gonic/gin" - "github.com/google/uuid" + "github.com/lithammer/shortuuid/v3" "os" "strings" "time" @@ -37,10 +37,10 @@ func (ctx *appContext) authenticate(gc *gin.Context) { return } claims, ok := token.Claims.(jwt.MapClaims) - var userId uuid.UUID + var userId string var jfId string if ok && token.Valid { - userId, _ = uuid.Parse(claims["id"].(string)) + userId = claims["id"].(string) jfId = claims["jfid"].(string) } else { respond(401, "Unauthorized", gc) @@ -59,7 +59,7 @@ func (ctx *appContext) authenticate(gc *gin.Context) { return } gc.Set("jfId", jfId) - gc.Set("userId", userId.String()) + gc.Set("userId", userId) gc.Next() } @@ -72,7 +72,7 @@ func (ctx *appContext) GetToken(gc *gin.Context) { auth, _ := base64.StdEncoding.DecodeString(header[1]) creds := strings.SplitN(string(auth), ":", 2) match := false - var userId uuid.UUID + var userId string for _, user := range ctx.users { if user.Username == creds[0] && user.Password == creds[1] { match = true @@ -101,7 +101,7 @@ func (ctx *appContext) GetToken(gc *gin.Context) { } } newuser := User{} - newuser.UserID, _ = uuid.NewRandom() + newuser.UserID = shortuuid.New() userId = newuser.UserID // uuid, nothing else identifiable! ctx.users = append(ctx.users, newuser) @@ -115,7 +115,7 @@ func (ctx *appContext) GetToken(gc *gin.Context) { gc.JSON(200, resp) } -func CreateToken(userId uuid.UUID, jfId string) (string, error) { +func CreateToken(userId string, jfId string) (string, error) { claims := jwt.MapClaims{ "valid": true, "id": userId, diff --git a/go.mod b/go.mod index a4aadc5..31834d0 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/labstack/echo/v4 v4.1.16 github.com/lestrrat-go/strftime v1.0.3 github.com/lib/pq v1.7.1 // indirect - github.com/lithammer/shortuuid/v3 v3.0.4 // indirect + github.com/lithammer/shortuuid/v3 v3.0.4 github.com/mailgun/mailgun-go v2.0.0+incompatible github.com/mailgun/mailgun-go/v4 v4.1.3 github.com/mattn/go-colorable v0.1.7 // indirect diff --git a/main.go b/main.go index d946ada..0d13ded 100644 --- a/main.go +++ b/main.go @@ -7,38 +7,40 @@ import ( "fmt" "github.com/gin-contrib/static" "github.com/gin-gonic/gin" - "github.com/google/uuid" + "github.com/lithammer/shortuuid/v3" "gopkg.in/ini.v1" "io/ioutil" + "log" "os" "path/filepath" ) // Username is JWT! type User struct { - UserID uuid.UUID `json:"id"` - Username string `json:"username"` - Password string `json:"password"` + UserID string `json:"id"` + Username string `json:"username"` + Password string `json:"password"` } type appContext struct { - config *ini.File - config_path string - configBase_path string - configBase map[string]interface{} - data_path string - local_path string - cssFile string - bsVersion int - jellyfinLogin bool - users []User - jf Jellyfin - authJf Jellyfin - datePattern string - timePattern string - storage Storage - validator Validator - email Emailer + config *ini.File + config_path string + configBase_path string + configBase map[string]interface{} + data_path string + local_path string + cssFile string + bsVersion int + jellyfinLogin bool + users []User + jf Jellyfin + authJf Jellyfin + datePattern string + timePattern string + storage Storage + validator Validator + email Emailer + info, debug, err *log.Logger } func GenerateSecret(length int) (string, error) { @@ -73,7 +75,20 @@ func main() { ctx.config_path = "/home/hrfee/.jf-accounts/config.ini" ctx.data_path = "/home/hrfee/.jf-accounts" ctx.local_path = "data" - ctx.loadConfig() + if ctx.loadConfig() != nil { + ctx.err.Fatalf("Failed to load config file \"%s\"", ctx.config_path) + } + + ctx.info = log.New(os.Stdout, "INFO: ", log.Ltime) + ctx.err = log.New(os.Stdout, "INFO: ", log.Ltime|log.Lshortfile) + if ctx.config.Section("ui").Key("debug").MustBool(true) { + ctx.debug = log.New(os.Stdout, "DEBUG: ", log.Ltime|log.Lshortfile) + } else { + ctx.debug = log.New(nil, "", 0) + } + + ctx.debug.Printf("Loaded config file \"%s\"", ctx.config_path) + if val, _ := ctx.config.Section("ui").Key("bs5").Bool(); val { ctx.cssFile = "bs5-jf.css" ctx.bsVersion = 5 @@ -82,6 +97,7 @@ func main() { ctx.bsVersion = 4 } // ctx.storage.formatter, _ = strftime.New("%Y-%m-%dT%H:%M:%S.%f") + ctx.debug.Println("Loading storage") ctx.storage.invite_path = filepath.Join(ctx.data_path, "invites.json") ctx.storage.loadInvites() ctx.storage.emails_path = filepath.Join(ctx.data_path, "emails.json") @@ -96,7 +112,7 @@ func main() { ctx.configBase_path = filepath.Join(ctx.local_path, "config-base.json") config_base, _ := ioutil.ReadFile(ctx.configBase_path) json.Unmarshal(config_base, &ctx.configBase) - //bson.UnmarshalExtJSON(config_base, true, &ctx.configBase) + themes := map[string]string{ "Jellyfin (Dark)": fmt.Sprintf("bs%d-jf.css", ctx.bsVersion), "Bootstrap (Light)": fmt.Sprintf("bs%d.css", ctx.bsVersion), @@ -105,23 +121,32 @@ func main() { if val, ok := themes[ctx.config.Section("ui").Key("theme").String()]; ok { ctx.cssFile = val } + ctx.debug.Printf("Using css file \"%s\"", ctx.cssFile) secret, err := GenerateSecret(16) if err != nil { - panic(err) + ctx.err.Fatal(err) } os.Setenv("JFA_SECRET", secret) ctx.jellyfinLogin = true if val, _ := ctx.config.Section("ui").Key("jellyfin_login").Bool(); !val { ctx.jellyfinLogin = false user := User{} - user.UserID, _ = uuid.NewUUID() + user.UserID = shortuuid.New() user.Username = ctx.config.Section("ui").Key("username").String() user.Password = ctx.config.Section("ui").Key("password").String() ctx.users = append(ctx.users, user) + } else { + ctx.debug.Println("Using Jellyfin for authentication") } + server := ctx.config.Section("jellyfin").Key("server").String() ctx.jf.init(server, "jfa-go", "0.1", "hrfee-arch", "hrfee-arch") - ctx.jf.authenticate(ctx.config.Section("jellyfin").Key("username").String(), ctx.config.Section("jellyfin").Key("password").String()) + var status int + _, status, err = ctx.jf.authenticate(ctx.config.Section("jellyfin").Key("username").String(), ctx.config.Section("jellyfin").Key("password").String()) + if status != 200 || err != nil { + ctx.err.Fatalf("Failed to authenticate with Jellyfin @ %s: Code %d", server, status) + } + ctx.info.Printf("Authenticated with %s", server) ctx.authJf.init(server, "jfa-go", "0.1", "auth", "auth") ctx.loadStrftime() @@ -133,7 +158,6 @@ func main() { "numbers": ctx.config.Section("password_validation").Key("number").MustInt(0), "special characters": ctx.config.Section("password_validation").Key("special").MustInt(0), } - if !ctx.config.Section("password_validation").Key("enabled").MustBool(false) { for key := range validatorConf { validatorConf[key] = 0 @@ -143,6 +167,7 @@ func main() { ctx.email.init(ctx) + ctx.debug.Println("Loading routes") router := gin.Default() router.Use(static.Serve("/", static.LocalFile("data/static", false))) router.Use(static.Serve("/invite/", static.LocalFile("data/static", false))) @@ -161,6 +186,7 @@ func main() { api.POST("/modifyUsers", ctx.ModifyEmails) api.POST("/setDefaults", ctx.SetDefaults) api.GET("/getConfig", ctx.GetConfig) - - router.Run(":8080") + api.POST("/modifyConfig", ctx.ModifyConfig) + addr := fmt.Sprintf("%s:%d", ctx.config.Section("ui").Key("host").String(), ctx.config.Section("ui").Key("port").MustInt(8056)) + router.Run(addr) }