From 961b9afa7532b9486d14e8299e9f43ad51c5ddfa Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Fri, 31 Jul 2020 12:48:37 +0100 Subject: [PATCH] Functioning user creation, notifications, Fixed password validation for new users, add invite route, couple other fixes. --- api.go | 144 +++++++++--- auth.go | 28 ++- data/created.html | 10 +- data/created.txt | 8 +- data/email.html | 10 +- data/email.txt | 8 +- data/expired.html | 4 +- data/expired.txt | 2 +- data/invite-email.html | 8 +- data/invite-email.txt | 6 +- .../form.html | 48 ++-- email.go | 209 ++++++++++++++++++ go.mod | 5 + go.sum | 22 ++ jfapi.go | 11 +- main.go | 9 +- pwval.go | 15 +- storage.go | 7 +- views.go | 23 +- 19 files changed, 469 insertions(+), 108 deletions(-) rename data/{currently-unused-templates => templates}/form.html (89%) create mode 100644 email.go diff --git a/api.go b/api.go index 540f6a8..396ee4c 100644 --- a/api.go +++ b/api.go @@ -17,10 +17,15 @@ func (ctx *appContext) loadStrftime() { return } +func (ctx *appContext) prettyTime(dt time.Time) (date, time string) { + date, _ = strtime.Strftime(dt, ctx.datePattern) + time, _ = strtime.Strftime(dt, ctx.timePattern) + return +} + func (ctx *appContext) formatDatetime(dt time.Time) string { - date, _ := strtime.Strftime(dt, ctx.datePattern) - time, _ := strtime.Strftime(dt, ctx.timePattern) - return date + time + d, t := ctx.prettyTime(dt) + return d + " " + t } // https://stackoverflow.com/questions/36530251/time-since-with-months-and-years/36531443#36531443 THANKS @@ -81,8 +86,18 @@ func (ctx *appContext) checkInvite(code string, used bool, username string) bool if current_time.After(expiry) { // NOTIFICATIONS notify := data.Notify - if ctx.config.Section("notifications").Key("Enabled").MustBool(false) && len(notify) != 0 { - fmt.Println("Notification Check (IMPLEMENT!)", notify) + if ctx.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 { + for address, settings := range notify { + if settings["notify-expiry"] { + if ctx.email.constructExpiry(invCode, data, ctx) != nil { + fmt.Println("failed expiry construct") + } else { + if ctx.email.send(address, ctx) != nil { + fmt.Println("failed expiry send") + } + } + } + } } changed = true fmt.Println("Deleting:", invCode) @@ -108,7 +123,6 @@ func (ctx *appContext) checkInvite(code string, used bool, username string) bool } } if changed { - fmt.Println("CHECKINVITES") ctx.storage.storeInvites() } return match @@ -118,20 +132,21 @@ func (ctx *appContext) checkInvite(code string, used bool, username string) bool // POST type newUserReq struct { - username string `json:"username"` - password string `json:"password"` - email string `json:"email"` - code string `json:"code"` + Username string `json:"username"` + Password string `json:"password"` + Email string `json:"email"` + Code string `json:"code"` } func (ctx *appContext) NewUser(gc *gin.Context) { var req newUserReq gc.BindJSON(&req) - if !ctx.checkInvite(req.code, false, "") { + if !ctx.checkInvite(req.Code, false, "") { gc.JSON(401, map[string]bool{"success": false}) gc.Abort() + return } - validation := ctx.validator.validate(req.password) + validation := ctx.validator.validate(req.Password) valid := true for _, val := range validation { if !val { @@ -140,20 +155,38 @@ func (ctx *appContext) NewUser(gc *gin.Context) { } if !valid { // 200 bcs idk what i did in js + fmt.Println("invalid") gc.JSON(200, validation) gc.Abort() + return } - existingUser, _, _ := ctx.jf.userByName(req.username, false) + existingUser, _, _ := ctx.jf.userByName(req.Username, false) if existingUser != nil { - respond(401, fmt.Sprintf("User already exists named %s", req.username), gc) + respond(401, fmt.Sprintf("User already exists named %s", req.Username), gc) + 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 { respond(401, "Unknown error", gc) + return + } + ctx.checkInvite(req.Code, true, req.Username) + invite := ctx.storage.invites[req.Code] + if ctx.config.Section("notifications").Key("enabled").MustBool(false) { + 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") + } else if ctx.email.send(address, ctx) != nil { + fmt.Println("created send failed") + } + } + } + } + var id string + if user["Id"] != nil { + id = user["Id"].(string) } - ctx.checkInvite(req.code, true, req.username) - // HANDLE NOTIFICATIONS! - id := user["Id"].(string) if len(ctx.storage.policy) != 0 { status, err = ctx.jf.setPolicy(id, ctx.storage.policy) if !(status == 200 || status == 204) { @@ -167,7 +200,7 @@ func (ctx *appContext) NewUser(gc *gin.Context) { } } if ctx.config.Section("password_resets").Key("enabled").MustBool(false) { - ctx.storage.emails[id] = req.email + ctx.storage.emails[id] = req.Email ctx.storage.storeEmails() } gc.JSON(200, validation) @@ -188,14 +221,12 @@ func (ctx *appContext) GenerateInvite(gc *gin.Context) { ctx.storage.loadInvites() gc.BindJSON(&req) current_time := time.Now() - fmt.Println("current time:", current_time) 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)) - fmt.Println("valid till:", valid_till) invite_code, _ := uuid.NewRandom() var invite Invite - invite.Created = ctx.formatDatetime(current_time) + invite.Created = current_time if req.MultipleUses { if req.NoLimit { invite.NoLimit = true @@ -207,8 +238,14 @@ func (ctx *appContext) GenerateInvite(gc *gin.Context) { } invite.ValidTill = valid_till if req.Email != "" && ctx.config.Section("invite_emails").Key("enabled").MustBool(false) { - invite.Email = fmt.Sprintf("Failed to send to %s", req.Email) - // EMAIL SEND! + invite.Email = req.Email + if err := ctx.email.constructInvite(invite_code.String(), invite, ctx); err != nil { + fmt.Println("error sending:", err) + invite.Email = fmt.Sprintf("Failed to send to %s", req.Email) + } 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.storage.invites[invite_code.String()] = invite fmt.Println("INVITES FROM API:", ctx.storage.invites) @@ -233,7 +270,7 @@ func (ctx *appContext) GetInvites(gc *gin.Context) { invite["days"] = days invite["hours"] = hours invite["minutes"] = minutes - invite["created"] = inv.Created + invite["created"] = ctx.formatDatetime(inv.Created) if len(inv.UsedBy) != 0 { invite["used-by"] = inv.UsedBy } @@ -251,7 +288,7 @@ func (ctx *appContext) GetInvites(gc *gin.Context) { var address string if ctx.config.Section("ui").Key("jellyfin_login").MustBool(false) { ctx.storage.loadEmails() - address = ctx.storage.emails[gc.GetString("userId")].(string) + address = ctx.storage.emails[gc.GetString("jfId")].(string) } else { address = ctx.config.Section("ui").Key("email").String() } @@ -270,3 +307,58 @@ func (ctx *appContext) GetInvites(gc *gin.Context) { } gc.JSON(200, resp) } + +type notifySetting struct { + NotifyExpiry bool `json:"notify-expiry"` + NotifyCreation bool `json:"notify-creation"` +} + +func (ctx *appContext) SetNotify(gc *gin.Context) { + var req map[string]notifySetting + gc.BindJSON(&req) + changed := false + for code, settings := range req { + ctx.storage.loadInvites() + ctx.storage.loadEmails() + invite, ok := ctx.storage.invites[code] + if !ok { + gc.JSON(400, map[string]string{"error": "Invalid invite code"}) + gc.Abort() + return + } + var address string + if ctx.config.Section("ui").Key("jellyfin_login").MustBool(false) { + var ok bool + address, ok = ctx.storage.emails[gc.GetString("jfId")].(string) + if !ok { + gc.JSON(500, map[string]string{"error": "Missing user email"}) + gc.Abort() + return + } + } else { + address = ctx.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 invite.Notify[address]["notify-expiry"] != settings.NotifyExpiry { + invite.Notify[address]["notify-expiry"] = settings.NotifyExpiry + changed = true + } + if invite.Notify[address]["notify-creation"] != settings.NotifyCreation { + invite.Notify[address]["notify-creation"] = settings.NotifyCreation + changed = true + } + if changed { + ctx.storage.invites[code] = invite + } + } + if changed { + ctx.storage.storeInvites() + } +} diff --git a/auth.go b/auth.go index d8c01be..02acc6b 100644 --- a/auth.go +++ b/auth.go @@ -38,8 +38,10 @@ func (ctx *appContext) authenticate(gc *gin.Context) { } claims, ok := token.Claims.(jwt.MapClaims) var userId uuid.UUID + var jfId string if ok && token.Valid { userId, _ = uuid.Parse(claims["id"].(string)) + jfId = claims["jfid"].(string) } else { respond(401, "Unauthorized", gc) return @@ -56,7 +58,8 @@ func (ctx *appContext) authenticate(gc *gin.Context) { respond(401, "Unauthorized", gc) return } - gc.Set("userId", userId) + gc.Set("jfId", jfId) + gc.Set("userId", userId.String()) gc.Next() } @@ -76,6 +79,7 @@ func (ctx *appContext) GetToken(gc *gin.Context) { userId = user.UserID } } + jfId := "" if !match { if !ctx.jellyfinLogin { respond(401, "Unauthorized", gc) @@ -84,18 +88,18 @@ func (ctx *appContext) GetToken(gc *gin.Context) { // eventually, make authenticate return a user to avoid two calls. var status int var err error - if ctx.config.Section("ui").Key("admin_only").MustBool(true) { - var user map[string]interface{} - user, status, err = ctx.jf.userByName(creds[0], false) - if !user["Policy"].(map[string]interface{})["IsAdministrator"].(bool) || !(status == 200 || status == 204) || err != nil { - respond(401, "Unauthorized", gc) - } - } - status, err = ctx.authJf.authenticate(creds[0], creds[1]) + var user map[string]interface{} + user, status, err = ctx.authJf.authenticate(creds[0], creds[1]) + jfId = user["Id"].(string) if status != 200 || err != nil { respond(401, "Unauthorized", gc) return } else { + if ctx.config.Section("ui").Key("admin_only").MustBool(true) { + if !user["Policy"].(map[string]interface{})["IsAdministrator"].(bool) { + respond(401, "Unauthorized", gc) + } + } newuser := User{} newuser.UserID, _ = uuid.NewRandom() userId = newuser.UserID @@ -103,7 +107,7 @@ func (ctx *appContext) GetToken(gc *gin.Context) { ctx.users = append(ctx.users, newuser) } } - token, err := CreateToken(userId) + token, err := CreateToken(userId, jfId) if err != nil { respond(500, "Error generating token", gc) } @@ -111,12 +115,14 @@ func (ctx *appContext) GetToken(gc *gin.Context) { gc.JSON(200, resp) } -func CreateToken(userId uuid.UUID) (string, error) { +func CreateToken(userId uuid.UUID, jfId string) (string, error) { claims := jwt.MapClaims{ "valid": true, "id": userId, "exp": time.Now().Add(time.Minute * 20).Unix(), + "jfid": jfId, } + tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) token, err := tk.SignedString([]byte(os.Getenv("JFA_SECRET"))) if err != nil { diff --git a/data/created.html b/data/created.html index 775a646..ab0b216 100644 --- a/data/created.html +++ b/data/created.html @@ -152,7 +152,7 @@

User Created

-

A user was created using code {{ code }}.

+

A user was created using code {{ .code }}.

@@ -165,9 +165,9 @@ Time - {{ username }} - {{ address }} - {{ time }} + {{ .username }} + {{ .address }} + {{ .time }} @@ -239,4 +239,4 @@ - \ No newline at end of file + diff --git a/data/created.txt b/data/created.txt index 154fbe0..7e3d3df 100644 --- a/data/created.txt +++ b/data/created.txt @@ -1,7 +1,7 @@ -A user was created using code {{ code }}. +A user was created using code {{ .code }}. -Name: {{ username }} -Address: {{ address }} -Time: {{ time }} +Name: {{ .username }} +Address: {{ .address }} +Time: {{ .time }} Note: Notification emails can be toggled on the admin dashboard. diff --git a/data/email.html b/data/email.html index bbc3cb3..10f9e68 100644 --- a/data/email.html +++ b/data/email.html @@ -151,10 +151,10 @@
-

Hi {{ username }},

+

Hi {{ .username }},

Someone has recently requested a password reset on Jellyfin.

If this was you, enter the below pin into the prompt.

-

The code will expire on {{ expiry_date }}, at {{ expiry_time }} UTC, which is in {{ expires_in }}.

+

The code will expire on {{ .expiry_date }}, at {{ .expiry_time }} UTC, which is in {{ .expires_in }}.

If this wasn't you, please ignore this email.

@@ -165,7 +165,7 @@

- {{ pin }} + {{ .pin }}

@@ -215,7 +215,7 @@
-
{{ message }}
+
{{ .message }}
@@ -240,4 +240,4 @@ - \ No newline at end of file + diff --git a/data/email.txt b/data/email.txt index c365d4d..6bd59f8 100644 --- a/data/email.txt +++ b/data/email.txt @@ -1,10 +1,10 @@ -Hi {{ username }}, +Hi {{ .username }}, Someone has recently requests a password reset on Jellyfin. If this was you, enter the below pin into the prompt. -This code will expire on {{ expiry_date }}, at {{ expiry_time }} UTC, which is in {{ expires_in }}. +This code will expire on {{ .expiry_date }}, at {{ .expiry_time }} UTC, which is in {{ .expires_in }}. If this wasn't you, please ignore this email. -PIN: {{ pin }} +PIN: {{ .pin }} -{{ message }} +{{ .message }} diff --git a/data/expired.html b/data/expired.html index 7d93dd5..2f76c54 100644 --- a/data/expired.html +++ b/data/expired.html @@ -152,7 +152,7 @@

Invite Expired.

-

Code {{ code }} expired at {{ expiry }}.

+

Code {{ .code }} expired at {{ .expiry }}.

@@ -224,4 +224,4 @@ - \ No newline at end of file + diff --git a/data/expired.txt b/data/expired.txt index 3541d16..b834152 100644 --- a/data/expired.txt +++ b/data/expired.txt @@ -1,5 +1,5 @@ Invite expired. -Code {{ code }} expired at {{ expiry }}. +Code {{ .code }} expired at {{ .expiry }}. Note: Notification emails can be toggled on the admin dashboard. diff --git a/data/invite-email.html b/data/invite-email.html index fc776f5..49f98b7 100644 --- a/data/invite-email.html +++ b/data/invite-email.html @@ -154,7 +154,7 @@

Hi,

You've been invited to Jellyfin.

To join, click the button below.

-

This invite will expire on {{ expiry_date }}, at {{ expiry_time }}, which is in {{ expires_in }}, so act quick.

+

This invite will expire on {{ .expiry_date }}, at {{ .expiry_time }}, which is in {{ .expires_in }}, so act quick.

@@ -163,7 +163,7 @@
- Setup your account + Setup your account
@@ -212,7 +212,7 @@
-
{{ message }}
+
{{ .message }}
@@ -237,4 +237,4 @@ - \ No newline at end of file + diff --git a/data/invite-email.txt b/data/invite-email.txt index 601e4a6..10855f1 100644 --- a/data/invite-email.txt +++ b/data/invite-email.txt @@ -1,8 +1,8 @@ Hi, You've been invited to Jellyfin. To join, follow the below link. -This invite will expire on {{ expiry_date }}, at {{ expiry_time }}, which is in {{ expires_in }}, so act quick. +This invite will expire on {{ .expiry_date }}, at {{ .expiry_time }}, which is in {{ .expires_in }}, so act quick. -{{ invite_link }} +{{ .invite_link }} -{{ message }} +{{ .message }} diff --git a/data/currently-unused-templates/form.html b/data/templates/form.html similarity index 89% rename from data/currently-unused-templates/form.html rename to data/templates/form.html index 4e0edb3..45bbe19 100644 --- a/data/currently-unused-templates/form.html +++ b/data/templates/form.html @@ -13,16 +13,16 @@ - - {% if not bs5 %} + + {{ if not .bs5 }} - {% endif %} + {{ end }} - {% if bs5 %} + {{ if .bs5 }} - {% else %} + {{ else }} - {% endif %} + {{ end }}