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

Settings functional, start adding logging

Modifying settings also formats it nicely, as a bonus.
Also we using shortuuid instead of normal uuidv4 now because its the same
length as what I used in the python version.
This commit is contained in:
Harvey Tindall 2020-07-31 22:07:09 +01:00
parent 024c0b56aa
commit 326b274329
4 changed files with 119 additions and 61 deletions

80
api.go
View File

@ -3,8 +3,9 @@ package main
import ( import (
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/knz/strtime" "github.com/knz/strtime"
"github.com/lithammer/shortuuid/v3"
"gopkg.in/ini.v1"
"time" "time"
) )
@ -82,25 +83,24 @@ func (ctx *appContext) checkInvite(code string, used bool, username string) bool
changed := false changed := false
for invCode, data := range ctx.storage.invites { for invCode, data := range ctx.storage.invites {
expiry := data.ValidTill expiry := data.ValidTill
fmt.Println("Expiry:", expiry)
if current_time.After(expiry) { if current_time.After(expiry) {
// NOTIFICATIONS ctx.debug.Printf("Housekeeping: Deleting old invite %s", code)
notify := data.Notify notify := data.Notify
if ctx.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 { if ctx.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 {
ctx.debug.Printf("%s: Expiry notification", code)
for address, settings := range notify { for address, settings := range notify {
if settings["notify-expiry"] { if settings["notify-expiry"] {
if ctx.email.constructExpiry(invCode, data, ctx) != nil { 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 { } else {
if ctx.email.send(address, ctx) != nil { ctx.info.Printf("Sent expiry notification to %s", address)
fmt.Println("failed expiry send")
}
} }
} }
} }
} }
changed = true changed = true
fmt.Println("Deleting:", invCode)
delete(ctx.storage.invites, invCode) delete(ctx.storage.invites, invCode)
} else if invCode == code { } else if invCode == code {
match = true match = true
@ -130,7 +130,6 @@ func (ctx *appContext) checkInvite(code string, used bool, username string) bool
// Routes from now on! // Routes from now on!
// POST
type newUserReq struct { type newUserReq struct {
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
@ -141,7 +140,9 @@ type newUserReq struct {
func (ctx *appContext) NewUser(gc *gin.Context) { func (ctx *appContext) NewUser(gc *gin.Context) {
var req newUserReq var req newUserReq
gc.BindJSON(&req) gc.BindJSON(&req)
ctx.debug.Printf("%s: New user attempt", req.Code)
if !ctx.checkInvite(req.Code, false, "") { 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.JSON(401, map[string]bool{"success": false})
gc.Abort() gc.Abort()
return return
@ -155,18 +156,21 @@ func (ctx *appContext) NewUser(gc *gin.Context) {
} }
if !valid { if !valid {
// 200 bcs idk what i did in js // 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.JSON(200, validation)
gc.Abort() gc.Abort()
return return
} }
existingUser, _, _ := ctx.jf.userByName(req.Username, false) existingUser, _, _ := ctx.jf.userByName(req.Username, false)
if existingUser != nil { 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 return
} }
user, status, err := ctx.jf.newUser(req.Username, req.Password) user, status, err := ctx.jf.newUser(req.Username, req.Password)
if !(status == 200 || status == 204) || err != nil { 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) respond(401, "Unknown error", gc)
return return
} }
@ -176,9 +180,13 @@ func (ctx *appContext) NewUser(gc *gin.Context) {
for address, settings := range invite.Notify { for address, settings := range invite.Notify {
if settings["notify-creation"] { if settings["notify-creation"] {
if ctx.email.constructCreated(req.Code, req.Username, req.Email, invite, ctx) != nil { 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 { } 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 { if len(ctx.storage.policy) != 0 {
status, err = ctx.jf.setPolicy(id, ctx.storage.policy) status, err = ctx.jf.setPolicy(id, ctx.storage.policy)
if !(status == 200 || status == 204) { 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 { if len(ctx.storage.configuration) != 0 && len(ctx.storage.displayprefs) != 0 {
status, err = ctx.jf.setConfiguration(id, ctx.storage.configuration) 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) 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) { if ctx.config.Section("password_resets").Key("enabled").MustBool(false) {
@ -213,18 +223,18 @@ type generateInviteReq struct {
Email string `json:"email"` Email string `json:"email"`
MultipleUses bool `json:"multiple-uses"` MultipleUses bool `json:"multiple-uses"`
NoLimit bool `json:"no-limit"` NoLimit bool `json:"no-limit"`
RemainingUses int `json:remaining-uses"` RemainingUses int `json:"remaining-uses"`
} }
func (ctx *appContext) GenerateInvite(gc *gin.Context) { func (ctx *appContext) GenerateInvite(gc *gin.Context) {
var req generateInviteReq var req generateInviteReq
ctx.debug.Println("Generating new invite")
ctx.storage.loadInvites() ctx.storage.loadInvites()
gc.BindJSON(&req) gc.BindJSON(&req)
current_time := time.Now() current_time := time.Now()
fmt.Println(req.Days, req.Hours, req.Minutes)
valid_till := current_time.AddDate(0, 0, req.Days) 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)) 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 var invite Invite
invite.Created = current_time invite.Created = current_time
if req.MultipleUses { if req.MultipleUses {
@ -238,22 +248,27 @@ func (ctx *appContext) GenerateInvite(gc *gin.Context) {
} }
invite.ValidTill = valid_till invite.ValidTill = valid_till
if req.Email != "" && ctx.config.Section("invite_emails").Key("enabled").MustBool(false) { 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 invite.Email = req.Email
if err := ctx.email.constructInvite(invite_code.String(), invite, ctx); err != nil { if err := ctx.email.constructInvite(invite_code, invite, ctx); err != nil {
fmt.Println("error sending:", err)
invite.Email = fmt.Sprintf("Failed to send to %s", req.Email) 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 { } 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) 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 ctx.storage.invites[invite_code] = invite
fmt.Println("INVITES FROM API:", ctx.storage.invites)
ctx.storage.storeInvites() ctx.storage.storeInvites()
fmt.Println("New inv")
gc.JSON(200, map[string]bool{"success": true}) gc.JSON(200, map[string]bool{"success": true})
} }
// logged up to here!
func (ctx *appContext) GetInvites(gc *gin.Context) { func (ctx *appContext) GetInvites(gc *gin.Context) {
current_time := time.Now() current_time := time.Now()
// checking one checks all of them // checking one checks all of them
@ -494,3 +509,20 @@ func (ctx *appContext) GetConfig(gc *gin.Context) {
} }
gc.JSON(200, resp) 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()
}

14
auth.go
View File

@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"github.com/dgrijalva/jwt-go" "github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/lithammer/shortuuid/v3"
"os" "os"
"strings" "strings"
"time" "time"
@ -37,10 +37,10 @@ func (ctx *appContext) authenticate(gc *gin.Context) {
return return
} }
claims, ok := token.Claims.(jwt.MapClaims) claims, ok := token.Claims.(jwt.MapClaims)
var userId uuid.UUID var userId string
var jfId string var jfId string
if ok && token.Valid { if ok && token.Valid {
userId, _ = uuid.Parse(claims["id"].(string)) userId = claims["id"].(string)
jfId = claims["jfid"].(string) jfId = claims["jfid"].(string)
} else { } else {
respond(401, "Unauthorized", gc) respond(401, "Unauthorized", gc)
@ -59,7 +59,7 @@ func (ctx *appContext) authenticate(gc *gin.Context) {
return return
} }
gc.Set("jfId", jfId) gc.Set("jfId", jfId)
gc.Set("userId", userId.String()) gc.Set("userId", userId)
gc.Next() gc.Next()
} }
@ -72,7 +72,7 @@ func (ctx *appContext) GetToken(gc *gin.Context) {
auth, _ := base64.StdEncoding.DecodeString(header[1]) auth, _ := base64.StdEncoding.DecodeString(header[1])
creds := strings.SplitN(string(auth), ":", 2) creds := strings.SplitN(string(auth), ":", 2)
match := false match := false
var userId uuid.UUID var userId string
for _, user := range ctx.users { for _, user := range ctx.users {
if user.Username == creds[0] && user.Password == creds[1] { if user.Username == creds[0] && user.Password == creds[1] {
match = true match = true
@ -101,7 +101,7 @@ func (ctx *appContext) GetToken(gc *gin.Context) {
} }
} }
newuser := User{} newuser := User{}
newuser.UserID, _ = uuid.NewRandom() newuser.UserID = shortuuid.New()
userId = newuser.UserID userId = newuser.UserID
// uuid, nothing else identifiable! // uuid, nothing else identifiable!
ctx.users = append(ctx.users, newuser) ctx.users = append(ctx.users, newuser)
@ -115,7 +115,7 @@ func (ctx *appContext) GetToken(gc *gin.Context) {
gc.JSON(200, resp) gc.JSON(200, resp)
} }
func CreateToken(userId uuid.UUID, jfId string) (string, error) { func CreateToken(userId string, jfId string) (string, error) {
claims := jwt.MapClaims{ claims := jwt.MapClaims{
"valid": true, "valid": true,
"id": userId, "id": userId,

2
go.mod
View File

@ -18,7 +18,7 @@ require (
github.com/labstack/echo/v4 v4.1.16 github.com/labstack/echo/v4 v4.1.16
github.com/lestrrat-go/strftime v1.0.3 github.com/lestrrat-go/strftime v1.0.3
github.com/lib/pq v1.7.1 // indirect 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 v2.0.0+incompatible
github.com/mailgun/mailgun-go/v4 v4.1.3 github.com/mailgun/mailgun-go/v4 v4.1.3
github.com/mattn/go-colorable v0.1.7 // indirect github.com/mattn/go-colorable v0.1.7 // indirect

84
main.go
View File

@ -7,38 +7,40 @@ import (
"fmt" "fmt"
"github.com/gin-contrib/static" "github.com/gin-contrib/static"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/lithammer/shortuuid/v3"
"gopkg.in/ini.v1" "gopkg.in/ini.v1"
"io/ioutil" "io/ioutil"
"log"
"os" "os"
"path/filepath" "path/filepath"
) )
// Username is JWT! // Username is JWT!
type User struct { type User struct {
UserID uuid.UUID `json:"id"` UserID string `json:"id"`
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
} }
type appContext struct { type appContext struct {
config *ini.File config *ini.File
config_path string config_path string
configBase_path string configBase_path string
configBase map[string]interface{} configBase map[string]interface{}
data_path string data_path string
local_path string local_path string
cssFile string cssFile string
bsVersion int bsVersion int
jellyfinLogin bool jellyfinLogin bool
users []User users []User
jf Jellyfin jf Jellyfin
authJf Jellyfin authJf Jellyfin
datePattern string datePattern string
timePattern string timePattern string
storage Storage storage Storage
validator Validator validator Validator
email Emailer email Emailer
info, debug, err *log.Logger
} }
func GenerateSecret(length int) (string, error) { func GenerateSecret(length int) (string, error) {
@ -73,7 +75,20 @@ func main() {
ctx.config_path = "/home/hrfee/.jf-accounts/config.ini" ctx.config_path = "/home/hrfee/.jf-accounts/config.ini"
ctx.data_path = "/home/hrfee/.jf-accounts" ctx.data_path = "/home/hrfee/.jf-accounts"
ctx.local_path = "data" 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 { if val, _ := ctx.config.Section("ui").Key("bs5").Bool(); val {
ctx.cssFile = "bs5-jf.css" ctx.cssFile = "bs5-jf.css"
ctx.bsVersion = 5 ctx.bsVersion = 5
@ -82,6 +97,7 @@ func main() {
ctx.bsVersion = 4 ctx.bsVersion = 4
} }
// ctx.storage.formatter, _ = strftime.New("%Y-%m-%dT%H:%M:%S.%f") // 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.invite_path = filepath.Join(ctx.data_path, "invites.json")
ctx.storage.loadInvites() ctx.storage.loadInvites()
ctx.storage.emails_path = filepath.Join(ctx.data_path, "emails.json") 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") ctx.configBase_path = filepath.Join(ctx.local_path, "config-base.json")
config_base, _ := ioutil.ReadFile(ctx.configBase_path) config_base, _ := ioutil.ReadFile(ctx.configBase_path)
json.Unmarshal(config_base, &ctx.configBase) json.Unmarshal(config_base, &ctx.configBase)
//bson.UnmarshalExtJSON(config_base, true, &ctx.configBase)
themes := map[string]string{ themes := map[string]string{
"Jellyfin (Dark)": fmt.Sprintf("bs%d-jf.css", ctx.bsVersion), "Jellyfin (Dark)": fmt.Sprintf("bs%d-jf.css", ctx.bsVersion),
"Bootstrap (Light)": fmt.Sprintf("bs%d.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 { if val, ok := themes[ctx.config.Section("ui").Key("theme").String()]; ok {
ctx.cssFile = val ctx.cssFile = val
} }
ctx.debug.Printf("Using css file \"%s\"", ctx.cssFile)
secret, err := GenerateSecret(16) secret, err := GenerateSecret(16)
if err != nil { if err != nil {
panic(err) ctx.err.Fatal(err)
} }
os.Setenv("JFA_SECRET", secret) os.Setenv("JFA_SECRET", secret)
ctx.jellyfinLogin = true ctx.jellyfinLogin = true
if val, _ := ctx.config.Section("ui").Key("jellyfin_login").Bool(); !val { if val, _ := ctx.config.Section("ui").Key("jellyfin_login").Bool(); !val {
ctx.jellyfinLogin = false ctx.jellyfinLogin = false
user := User{} user := User{}
user.UserID, _ = uuid.NewUUID() user.UserID = shortuuid.New()
user.Username = ctx.config.Section("ui").Key("username").String() user.Username = ctx.config.Section("ui").Key("username").String()
user.Password = ctx.config.Section("ui").Key("password").String() user.Password = ctx.config.Section("ui").Key("password").String()
ctx.users = append(ctx.users, user) ctx.users = append(ctx.users, user)
} else {
ctx.debug.Println("Using Jellyfin for authentication")
} }
server := ctx.config.Section("jellyfin").Key("server").String() server := ctx.config.Section("jellyfin").Key("server").String()
ctx.jf.init(server, "jfa-go", "0.1", "hrfee-arch", "hrfee-arch") 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.authJf.init(server, "jfa-go", "0.1", "auth", "auth")
ctx.loadStrftime() ctx.loadStrftime()
@ -133,7 +158,6 @@ func main() {
"numbers": ctx.config.Section("password_validation").Key("number").MustInt(0), "numbers": ctx.config.Section("password_validation").Key("number").MustInt(0),
"special characters": ctx.config.Section("password_validation").Key("special").MustInt(0), "special characters": ctx.config.Section("password_validation").Key("special").MustInt(0),
} }
if !ctx.config.Section("password_validation").Key("enabled").MustBool(false) { if !ctx.config.Section("password_validation").Key("enabled").MustBool(false) {
for key := range validatorConf { for key := range validatorConf {
validatorConf[key] = 0 validatorConf[key] = 0
@ -143,6 +167,7 @@ func main() {
ctx.email.init(ctx) ctx.email.init(ctx)
ctx.debug.Println("Loading routes")
router := gin.Default() router := gin.Default()
router.Use(static.Serve("/", static.LocalFile("data/static", false))) router.Use(static.Serve("/", static.LocalFile("data/static", false)))
router.Use(static.Serve("/invite/", 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("/modifyUsers", ctx.ModifyEmails)
api.POST("/setDefaults", ctx.SetDefaults) api.POST("/setDefaults", ctx.SetDefaults)
api.GET("/getConfig", ctx.GetConfig) api.GET("/getConfig", ctx.GetConfig)
api.POST("/modifyConfig", ctx.ModifyConfig)
router.Run(":8080") addr := fmt.Sprintf("%s:%d", ctx.config.Section("ui").Key("host").String(), ctx.config.Section("ui").Key("port").MustInt(8056))
router.Run(addr)
} }