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

Functioning user creation, notifications,

Fixed password validation for new users, add invite route, couple other
fixes.
This commit is contained in:
Harvey Tindall 2020-07-31 12:48:37 +01:00
parent d8fb6e5613
commit 961b9afa75
19 changed files with 469 additions and 108 deletions

142
api.go
View File

@ -17,10 +17,15 @@ func (ctx *appContext) loadStrftime() {
return 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 { func (ctx *appContext) formatDatetime(dt time.Time) string {
date, _ := strtime.Strftime(dt, ctx.datePattern) d, t := ctx.prettyTime(dt)
time, _ := strtime.Strftime(dt, ctx.timePattern) return d + " " + t
return date + time
} }
// https://stackoverflow.com/questions/36530251/time-since-with-months-and-years/36531443#36531443 THANKS // 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) { if current_time.After(expiry) {
// NOTIFICATIONS // NOTIFICATIONS
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 {
fmt.Println("Notification Check (IMPLEMENT!)", notify) 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 changed = true
fmt.Println("Deleting:", invCode) fmt.Println("Deleting:", invCode)
@ -108,7 +123,6 @@ func (ctx *appContext) checkInvite(code string, used bool, username string) bool
} }
} }
if changed { if changed {
fmt.Println("CHECKINVITES")
ctx.storage.storeInvites() ctx.storage.storeInvites()
} }
return match return match
@ -118,20 +132,21 @@ func (ctx *appContext) checkInvite(code string, used bool, username string) bool
// POST // POST
type newUserReq struct { type newUserReq struct {
username string `json:"username"` Username string `json:"username"`
password string `json:"password"` Password string `json:"password"`
email string `json:"email"` Email string `json:"email"`
code string `json:"code"` Code string `json:"code"`
} }
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)
if !ctx.checkInvite(req.code, false, "") { if !ctx.checkInvite(req.Code, false, "") {
gc.JSON(401, map[string]bool{"success": false}) gc.JSON(401, map[string]bool{"success": false})
gc.Abort() gc.Abort()
return
} }
validation := ctx.validator.validate(req.password) validation := ctx.validator.validate(req.Password)
valid := true valid := true
for _, val := range validation { for _, val := range validation {
if !val { if !val {
@ -140,20 +155,38 @@ 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")
gc.JSON(200, validation) gc.JSON(200, validation)
gc.Abort() gc.Abort()
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) 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 { if !(status == 200 || status == 204) || err != nil {
respond(401, "Unknown error", gc) 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 { 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) {
@ -167,7 +200,7 @@ func (ctx *appContext) NewUser(gc *gin.Context) {
} }
} }
if ctx.config.Section("password_resets").Key("enabled").MustBool(false) { 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() ctx.storage.storeEmails()
} }
gc.JSON(200, validation) gc.JSON(200, validation)
@ -188,14 +221,12 @@ func (ctx *appContext) GenerateInvite(gc *gin.Context) {
ctx.storage.loadInvites() ctx.storage.loadInvites()
gc.BindJSON(&req) gc.BindJSON(&req)
current_time := time.Now() current_time := time.Now()
fmt.Println("current time:", current_time)
fmt.Println(req.Days, req.Hours, req.Minutes) 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))
fmt.Println("valid till:", valid_till)
invite_code, _ := uuid.NewRandom() invite_code, _ := uuid.NewRandom()
var invite Invite var invite Invite
invite.Created = ctx.formatDatetime(current_time) invite.Created = current_time
if req.MultipleUses { if req.MultipleUses {
if req.NoLimit { if req.NoLimit {
invite.NoLimit = true invite.NoLimit = true
@ -207,8 +238,14 @@ 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) {
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) invite.Email = fmt.Sprintf("Failed to send to %s", req.Email)
// EMAIL SEND! } 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 ctx.storage.invites[invite_code.String()] = invite
fmt.Println("INVITES FROM API:", ctx.storage.invites) fmt.Println("INVITES FROM API:", ctx.storage.invites)
@ -233,7 +270,7 @@ func (ctx *appContext) GetInvites(gc *gin.Context) {
invite["days"] = days invite["days"] = days
invite["hours"] = hours invite["hours"] = hours
invite["minutes"] = minutes invite["minutes"] = minutes
invite["created"] = inv.Created invite["created"] = ctx.formatDatetime(inv.Created)
if len(inv.UsedBy) != 0 { if len(inv.UsedBy) != 0 {
invite["used-by"] = inv.UsedBy invite["used-by"] = inv.UsedBy
} }
@ -251,7 +288,7 @@ func (ctx *appContext) GetInvites(gc *gin.Context) {
var address string var address string
if ctx.config.Section("ui").Key("jellyfin_login").MustBool(false) { if ctx.config.Section("ui").Key("jellyfin_login").MustBool(false) {
ctx.storage.loadEmails() ctx.storage.loadEmails()
address = ctx.storage.emails[gc.GetString("userId")].(string) address = ctx.storage.emails[gc.GetString("jfId")].(string)
} else { } else {
address = ctx.config.Section("ui").Key("email").String() address = ctx.config.Section("ui").Key("email").String()
} }
@ -270,3 +307,58 @@ func (ctx *appContext) GetInvites(gc *gin.Context) {
} }
gc.JSON(200, resp) 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()
}
}

26
auth.go
View File

@ -38,8 +38,10 @@ func (ctx *appContext) authenticate(gc *gin.Context) {
} }
claims, ok := token.Claims.(jwt.MapClaims) claims, ok := token.Claims.(jwt.MapClaims)
var userId uuid.UUID var userId uuid.UUID
var jfId string
if ok && token.Valid { if ok && token.Valid {
userId, _ = uuid.Parse(claims["id"].(string)) userId, _ = uuid.Parse(claims["id"].(string))
jfId = claims["jfid"].(string)
} else { } else {
respond(401, "Unauthorized", gc) respond(401, "Unauthorized", gc)
return return
@ -56,7 +58,8 @@ func (ctx *appContext) authenticate(gc *gin.Context) {
respond(401, "Unauthorized", gc) respond(401, "Unauthorized", gc)
return return
} }
gc.Set("userId", userId) gc.Set("jfId", jfId)
gc.Set("userId", userId.String())
gc.Next() gc.Next()
} }
@ -76,6 +79,7 @@ func (ctx *appContext) GetToken(gc *gin.Context) {
userId = user.UserID userId = user.UserID
} }
} }
jfId := ""
if !match { if !match {
if !ctx.jellyfinLogin { if !ctx.jellyfinLogin {
respond(401, "Unauthorized", gc) 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. // eventually, make authenticate return a user to avoid two calls.
var status int var status int
var err error var err error
if ctx.config.Section("ui").Key("admin_only").MustBool(true) {
var user map[string]interface{} var user map[string]interface{}
user, status, err = ctx.jf.userByName(creds[0], false) user, status, err = ctx.authJf.authenticate(creds[0], creds[1])
if !user["Policy"].(map[string]interface{})["IsAdministrator"].(bool) || !(status == 200 || status == 204) || err != nil { jfId = user["Id"].(string)
respond(401, "Unauthorized", gc)
}
}
status, err = ctx.authJf.authenticate(creds[0], creds[1])
if status != 200 || err != nil { if status != 200 || err != nil {
respond(401, "Unauthorized", gc) respond(401, "Unauthorized", gc)
return return
} else { } 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 := User{}
newuser.UserID, _ = uuid.NewRandom() newuser.UserID, _ = uuid.NewRandom()
userId = newuser.UserID userId = newuser.UserID
@ -103,7 +107,7 @@ func (ctx *appContext) GetToken(gc *gin.Context) {
ctx.users = append(ctx.users, newuser) ctx.users = append(ctx.users, newuser)
} }
} }
token, err := CreateToken(userId) token, err := CreateToken(userId, jfId)
if err != nil { if err != nil {
respond(500, "Error generating token", gc) respond(500, "Error generating token", gc)
} }
@ -111,12 +115,14 @@ func (ctx *appContext) GetToken(gc *gin.Context) {
gc.JSON(200, resp) gc.JSON(200, resp)
} }
func CreateToken(userId uuid.UUID) (string, error) { func CreateToken(userId uuid.UUID, jfId string) (string, error) {
claims := jwt.MapClaims{ claims := jwt.MapClaims{
"valid": true, "valid": true,
"id": userId, "id": userId,
"exp": time.Now().Add(time.Minute * 20).Unix(), "exp": time.Now().Add(time.Minute * 20).Unix(),
"jfid": jfId,
} }
tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
token, err := tk.SignedString([]byte(os.Getenv("JFA_SECRET"))) token, err := tk.SignedString([]byte(os.Getenv("JFA_SECRET")))
if err != nil { if err != nil {

View File

@ -152,7 +152,7 @@
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Noto Sans, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:rgba(255,255,255,0.8);"> <div style="font-family:Noto Sans, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:rgba(255,255,255,0.8);">
<h3>User Created</h3> <h3>User Created</h3>
<p>A user was created using code {{ code }}.</p> <p>A user was created using code {{ .code }}.</p>
</div> </div>
</td> </td>
</tr> </tr>
@ -165,9 +165,9 @@
<th>Time</th> <th>Time</th>
</tr> </tr>
<tr style="font-style: italic; text-align: left; color: rgb(153,153,153);"> <tr style="font-style: italic; text-align: left; color: rgb(153,153,153);">
<th>{{ username }}</th> <th>{{ .username }}</th>
<th>{{ address }}</th> <th>{{ .address }}</th>
<th>{{ time }}</th> <th>{{ .time }}</th>
</table> </table>
</td> </td>
</tr> </tr>

View File

@ -1,7 +1,7 @@
A user was created using code {{ code }}. A user was created using code {{ .code }}.
Name: {{ username }} Name: {{ .username }}
Address: {{ address }} Address: {{ .address }}
Time: {{ time }} Time: {{ .time }}
Note: Notification emails can be toggled on the admin dashboard. Note: Notification emails can be toggled on the admin dashboard.

View File

@ -151,10 +151,10 @@
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Noto Sans, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:rgba(255,255,255,0.8);"> <div style="font-family:Noto Sans, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:rgba(255,255,255,0.8);">
<p>Hi {{ username }},</p> <p>Hi {{ .username }},</p>
<p> Someone has recently requested a password reset on Jellyfin.</p> <p> Someone has recently requested a password reset on Jellyfin.</p>
<p>If this was you, enter the below pin into the prompt.</p> <p>If this was you, enter the below pin into the prompt.</p>
<p>The code will expire on {{ expiry_date }}, at {{ expiry_time }} UTC, which is in {{ expires_in }}.</p> <p>The code will expire on {{ .expiry_date }}, at {{ .expiry_time }} UTC, which is in {{ .expires_in }}.</p>
<p>If this wasn't you, please ignore this email.</p> <p>If this wasn't you, please ignore this email.</p>
</div> </div>
</td> </td>
@ -165,7 +165,7 @@
<tr> <tr>
<td align="center" bgcolor="rgb(0,164,220)" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:rgb(0,164,220);" valign="middle"> <td align="center" bgcolor="rgb(0,164,220)" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:rgb(0,164,220);" valign="middle">
<p style="display:inline-block;background:rgb(0,164,220);color:rgba(255,255,255,0.87);font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"> <p style="display:inline-block;background:rgb(0,164,220);color:rgba(255,255,255,0.87);font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;">
{{ pin }} {{ .pin }}
</p> </p>
</td> </td>
</tr> </tr>
@ -215,7 +215,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"> <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:14px;font-style:italic;line-height:1;text-align:left;color:rgb(153,153,153);">{{ message }}</div> <div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:14px;font-style:italic;line-height:1;text-align:left;color:rgb(153,153,153);">{{ .message }}</div>
</td> </td>
</tr> </tr>
</table> </table>

View File

@ -1,10 +1,10 @@
Hi {{ username }}, Hi {{ .username }},
Someone has recently requests a password reset on Jellyfin. Someone has recently requests a password reset on Jellyfin.
If this was you, enter the below pin into the prompt. 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. If this wasn't you, please ignore this email.
PIN: {{ pin }} PIN: {{ .pin }}
{{ message }} {{ .message }}

View File

@ -152,7 +152,7 @@
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Noto Sans, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:rgba(255,255,255,0.8);"> <div style="font-family:Noto Sans, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:rgba(255,255,255,0.8);">
<h3>Invite Expired.</h3> <h3>Invite Expired.</h3>
<p>Code {{ code }} expired at {{ expiry }}.</p> <p>Code {{ .code }} expired at {{ .expiry }}.</p>
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -1,5 +1,5 @@
Invite expired. Invite expired.
Code {{ code }} expired at {{ expiry }}. Code {{ .code }} expired at {{ .expiry }}.
Note: Notification emails can be toggled on the admin dashboard. Note: Notification emails can be toggled on the admin dashboard.

View File

@ -154,7 +154,7 @@
<p>Hi,</p> <p>Hi,</p>
<h3>You've been invited to Jellyfin.</h3> <h3>You've been invited to Jellyfin.</h3>
<p>To join, click the button below.</p> <p>To join, click the button below.</p>
<p>This invite will expire on {{ expiry_date }}, at {{ expiry_time }}, which is in {{ expires_in }}, so act quick.</p> <p>This invite will expire on {{ .expiry_date }}, at {{ .expiry_time }}, which is in {{ .expires_in }}, so act quick.</p>
</div> </div>
</td> </td>
</tr> </tr>
@ -163,7 +163,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;"> <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
<tr> <tr>
<td align="center" bgcolor="rgb(0,164,220)" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:rgb(0,164,220);" valign="middle"> <td align="center" bgcolor="rgb(0,164,220)" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:rgb(0,164,220);" valign="middle">
<a href="{{ invite_link }}" style="display:inline-block;background:rgb(0,164,220);color:rgba(255,255,255,0.87);font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;" target="_blank"> Setup your account </a> <a href="{{ .invite_link }}" style="display:inline-block;background:rgb(0,164,220);color:rgba(255,255,255,0.87);font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;" target="_blank"> Setup your account </a>
</td> </td>
</tr> </tr>
</table> </table>
@ -212,7 +212,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"> <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:14px;font-style:italic;line-height:1;text-align:left;color:rgb(153,153,153);">{{ message }}</div> <div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:14px;font-style:italic;line-height:1;text-align:left;color:rgb(153,153,153);">{{ .message }}</div>
</td> </td>
</tr> </tr>
</table> </table>

View File

@ -1,8 +1,8 @@
Hi, Hi,
You've been invited to Jellyfin. You've been invited to Jellyfin.
To join, follow the below link. 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 }}

View File

@ -13,16 +13,16 @@
<meta name="theme-color" content="#ffffff"> <meta name="theme-color" content="#ffffff">
<!-- Bootstrap CSS --> <!-- Bootstrap CSS -->
<link rel="stylesheet" type="text/css" href="{{ css_file }}"> <link rel="stylesheet" type="text/css" href="{{ .cssFile }}">
{% if not bs5 %} {{ if not .bs5 }}
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script> <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
{% endif %} {{ end }}
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
{% if bs5 %} {{ if .bs5 }}
<script src="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/js/bootstrap.min.js" integrity="sha384-oesi62hOLfzrys4LxRF63OJCXdXDipiYWBnvTl9Y9/TRlw5xlKIEHpNyvvDShgf/" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/js/bootstrap.min.js" integrity="sha384-oesi62hOLfzrys4LxRF63OJCXdXDipiYWBnvTl9Y9/TRlw5xlKIEHpNyvvDShgf/" crossorigin="anonymous"></script>
{% else %} {{ else }}
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
{% endif %} {{ end }}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<style> <style>
.pageContainer { .pageContainer {
@ -51,10 +51,10 @@
<h5 class="modal-title" id="successTitle">Success!</h5> <h5 class="modal-title" id="successTitle">Success!</h5>
</div> </div>
<div class="modal-body" id="successBody"> <div class="modal-body" id="successBody">
<p>{{ successMessage }}</p> <p>{{ .successMessage }}</p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<a href="{{ jfLink }}" class="btn btn-primary">Continue</a> <a href="{{ .jfLink }}" class="btn btn-primary">Continue</a>
</div> </div>
</div> </div>
</div> </div>
@ -63,8 +63,8 @@
<h1> <h1>
Create Account Create Account
</h1> </h1>
<p>{{ helpMessage }}</p> <p>{{ .helpMessage }}</p>
<p class="contactBox">{{ contactMessage }}</p> <p class="contactBox">{{ .contactMessage }}</p>
<div class="container" id="container"> <div class="container" id="container">
<div class="row" id="cardContainer"> <div class="row" id="cardContainer">
<div class="col-sm"> <div class="col-sm">
@ -74,14 +74,14 @@
<form action="#" method="POST" id="accountForm"> <form action="#" method="POST" id="accountForm">
<div class="form-group"> <div class="form-group">
<label for="inputEmail">Email</label> <label for="inputEmail">Email</label>
<input type="email" class="form-control" id="{% if username %}inputEmail{% else %}inputUsername{% endif %}" name="{% if username %}email{% else %}username{% endif %}" placeholder="Email" value="{{ email }}" required> <input type="email" class="form-control" id="{{ if .username }}inputEmail{{ else }}inputUsername{{ end }}" name="{{ if .username }}email{{ else }}username{{ end }}" placeholder="Email" value="{{ .email }}" required>
</div> </div>
{% if username %} {{ if .username }}
<div class="form-group"> <div class="form-group">
<label for="inputUsername">Username</label> <label for="inputUsername">Username</label>
<input type="username" class="form-control" id="inputUsername" name="username" placeholder="Username" required> <input type="username" class="form-control" id="inputUsername" name="username" placeholder="Username" required>
</div> </div>
{% endif %} {{ end }}
<div class="form-group"> <div class="form-group">
<label for="inputPassword">Password</label> <label for="inputPassword">Password</label>
<input type="password" class="form-control" id="inputPassword" name="password" placeholder="Password" required> <input type="password" class="form-control" id="inputPassword" name="password" placeholder="Password" required>
@ -95,32 +95,32 @@
</div> </div>
</div> </div>
</div> </div>
{% if validate %} {{ if .validate }}
<div class="col-sm" id="requirementBox"> <div class="col-sm" id="requirementBox">
<div class="card mb-3 requirementBox"> <div class="card mb-3 requirementBox">
<div class="card-header">Password Requirements</div> <div class="card-header">Password Requirements</div>
<div class="card-body"> <div class="card-body">
<ul class="list-group"> <ul class="list-group">
{% for key, value in requirements.items() %} {{ range $key, $value := .requirements }}
<li id="{{ key }}" class="list-group-item list-group-item-danger"> <li id="{{ $key }}" class="list-group-item list-group-item-danger">
<div> {{ value }}</div> <div> {{ $value }}</div>
</li> </li>
{% endfor %} {{ end }}
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
{% endif %} {{ end }}
</div> </div>
</div> </div>
</div> </div>
<script src="serialize.js"></script> <script src="serialize.js"></script>
<script> <script>
{% if bs5 %} {{ if .bs5 }}
var bsVersion = 5; var bsVersion = 5;
{% else %} {{ else }}
var bsVersion = 4; var bsVersion = 4;
{% endif %} {{ end }}
if (bsVersion == 5) { if (bsVersion == 5) {
var successBox = new bootstrap.Modal(document.getElementById('successBox')); var successBox = new bootstrap.Modal(document.getElementById('successBox'));
} else if (bsVersion == 4) { } else if (bsVersion == 4) {
@ -162,9 +162,9 @@
toggleSpinner(); toggleSpinner();
var send = serializeForm('accountForm'); var send = serializeForm('accountForm');
send['code'] = code; send['code'] = code;
{% if not username %} {{ if not .username }}
send['email'] = send['username']; send['email'] = send['username'];
{% endif %} {{ end }}
send = JSON.stringify(send); send = JSON.stringify(send);
var req = new XMLHttpRequest(); var req = new XMLHttpRequest();
req.open("POST", "/newUser", true); req.open("POST", "/newUser", true);

209
email.go Normal file
View File

@ -0,0 +1,209 @@
package main
import (
"bytes"
// "context"
"context"
"fmt"
"github.com/knz/strtime"
"github.com/mailgun/mailgun-go/v4"
"html/template"
"net/smtp"
"strings"
"time"
)
type Emailer struct {
smtpAuth smtp.Auth
sendType, sendMethod, fromAddr, fromName string
content Email
mg *mailgun.MailgunImpl
}
type Email struct {
subject string
html, text string
}
func (email *Emailer) formatExpiry(expiry time.Time, tzaware bool, datePattern, timePattern string) (d, t, expires_in string) {
d, _ = strtime.Strftime(expiry, datePattern)
t, _ = strtime.Strftime(expiry, timePattern)
current_time := time.Now()
if tzaware {
current_time = current_time.UTC()
}
_, _, days, hours, minutes, _ := timeDiff(expiry, current_time)
if days != 0 {
expires_in += fmt.Sprintf("%dd ", days)
}
if hours != 0 {
expires_in += fmt.Sprintf("%dh ", hours)
}
if minutes != 0 {
expires_in += fmt.Sprintf("%dm ", minutes)
}
expires_in = strings.TrimSuffix(expires_in, " ")
return
}
func (email *Emailer) init(ctx *appContext) {
email.fromAddr = ctx.config.Section("email").Key("address").String()
email.fromName = ctx.config.Section("email").Key("from").String()
email.sendMethod = ctx.config.Section("email").Key("method").String()
if email.sendMethod == "mailgun" {
email.mg = mailgun.NewMailgun(strings.Split(email.fromAddr, "@")[1], ctx.config.Section("mailgun").Key("api_key").String())
api_url := ctx.config.Section("mailgun").Key("api_url").String()
if strings.Contains(api_url, "messages") {
api_url = api_url[0:strings.LastIndex(api_url, "/")]
api_url = api_url[0:strings.LastIndex(api_url, "/")]
}
email.mg.SetAPIBase(api_url)
}
}
func (email *Emailer) constructInvite(code string, invite Invite, ctx *appContext) error {
email.content.subject = ctx.config.Section("invite_emails").Key("subject").String()
expiry := invite.ValidTill
d, t, expires_in := email.formatExpiry(expiry, false, ctx.datePattern, ctx.timePattern)
message := ctx.config.Section("email").Key("message").String()
invite_link := ctx.config.Section("invite_emails").Key("url_base").String()
invite_link = fmt.Sprintf("%s/%s", invite_link, code)
for _, key := range []string{"html", "text"} {
fpath := ctx.config.Section("invite_emails").Key("email_" + key).String()
tpl, err := template.ParseFiles(fpath)
if err != nil {
fmt.Println("failed email", err)
return err
}
var tplData bytes.Buffer
err = tpl.Execute(&tplData, map[string]string{
"expiry_date": d,
"expiry_time": t,
"expires_in": expires_in,
"invite_link": invite_link,
"message": message,
})
if err != nil {
fmt.Println("failed email", err)
return err
}
if key == "html" {
email.content.html = tplData.String()
} else {
email.content.text = tplData.String()
}
}
email.sendType = "invite"
return nil
}
func (email *Emailer) constructExpiry(code string, invite Invite, ctx *appContext) error {
email.content.subject = "Notice: Invite expired"
expiry := ctx.formatDatetime(invite.ValidTill)
for _, key := range []string{"html", "text"} {
fpath := ctx.config.Section("notifications").Key("expiry_" + key).String()
tpl, err := template.ParseFiles(fpath)
if err != nil {
fmt.Println("failed email", err)
return err
}
var tplData bytes.Buffer
err = tpl.Execute(&tplData, map[string]string{
"code": code,
"expiry": expiry,
})
if err != nil {
fmt.Println("failed email", err)
return err
}
if key == "html" {
email.content.html = tplData.String()
} else {
email.content.text = tplData.String()
}
}
email.sendType = "expiry"
return nil
}
func (email *Emailer) constructCreated(code, username, address string, invite Invite, ctx *appContext) error {
email.content.subject = "Notice: User created"
created := ctx.formatDatetime(invite.Created)
var tplAddress string
if ctx.config.Section("email").Key("no_username").MustBool(false) {
tplAddress = "n/a"
} else {
tplAddress = address
}
for _, key := range []string{"html", "text"} {
fpath := ctx.config.Section("notifications").Key("created_" + key).String()
tpl, err := template.ParseFiles(fpath)
if err != nil {
fmt.Println("failed email", err)
return err
}
var tplData bytes.Buffer
err = tpl.Execute(&tplData, map[string]string{
"code": code,
"username": username,
"address": tplAddress,
"time": created,
})
if err != nil {
fmt.Println("failed email", err)
return err
}
if key == "html" {
email.content.html = tplData.String()
} else {
email.content.text = tplData.String()
}
}
email.sendType = "created"
return nil
}
func (email *Emailer) send(address string, ctx *appContext) error {
if email.sendMethod == "mailgun" {
// reqData := map[string]string{
// "to": fmt.Sprintf("%s <%s>", "test", email.to),
// "from": email.fromAddr,
// "subject": email.subject,
// }
// if email.sendType == "invite" {
// reqData["text"] = email.invite.text
// reqData["html"] = email.invite.html
// }
// data := &bytes.Buffer{}
// encoder := json.NewEncoder(data)
// encoder.SetEscapeHTML(false)
// err := encoder.Encode(reqData)
// fmt.Println("marshaled:", data)
// if err != nil {
// fmt.Println("Failed marshal:", err, ">", data)
// return err
// }
// var req *http.Request
// req, err = http.NewRequest("POST", ctx.config.Section("mailgun").Key("api_url").String(), data)
// req.SetBasicAuth("api", ctx.config.Section("mailgun").Key("api_key").String())
// var resp *http.Response
// resp, err = email.httpClient.Do(req)
// if err != nil || !(resp.StatusCode == 200 || resp.StatusCode == 204) {
// fmt.Println("failed send:", err, resp.StatusCode)
// fmt.Println("resp:", resp.Header.Get("Content-Encoding"), resp.Body)
// }
message := email.mg.NewMessage(
fmt.Sprintf("%s <%s>", email.fromName, email.fromAddr),
email.content.subject,
email.content.text,
address)
message.SetHtml(email.content.html)
mgctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
_, id, err := email.mg.Send(mgctx, message)
fmt.Println("mailgun:", id, err)
}
return nil
}

5
go.mod
View File

@ -6,16 +6,21 @@ require (
github.com/astaxie/beego v1.12.2 github.com/astaxie/beego v1.12.2
github.com/beego/bee v1.12.0 // indirect github.com/beego/bee v1.12.0 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
github.com/emersion/go-smtp v0.13.0
github.com/flosch/pongo2 v0.0.0-20200529170236-5abacdfa4915 github.com/flosch/pongo2 v0.0.0-20200529170236-5abacdfa4915
github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2 github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2
github.com/gin-gonic/gin v1.6.3 github.com/gin-gonic/gin v1.6.3
github.com/go-playground/validator/v10 v10.3.0 // indirect github.com/go-playground/validator/v10 v10.3.0 // indirect
github.com/gobuffalo/envy v1.9.0 // indirect
github.com/google/uuid v1.1.1 github.com/google/uuid v1.1.1
github.com/knz/strtime v0.0.0-20200318182718-be999391ffa9 github.com/knz/strtime v0.0.0-20200318182718-be999391ffa9
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 // indirect
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 github.com/mattn/go-colorable v0.1.7 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/mitchellh/mapstructure v1.3.3 // indirect github.com/mitchellh/mapstructure v1.3.3 // indirect

22
go.sum
View File

@ -74,8 +74,15 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/elastic/go-elasticsearch/v6 v6.8.5/go.mod h1:UwaDJsD3rWLM5rKNFzv9hgox93HoX8utj1kxD9aFUcI= github.com/elastic/go-elasticsearch/v6 v6.8.5/go.mod h1:UwaDJsD3rWLM5rKNFzv9hgox93HoX8utj1kxD9aFUcI=
github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.13.0 h1:aC3Kc21TdfvXnuJXCQXuhnDXUldhc12qME/S7Y3Y94g=
github.com/emersion/go-smtp v0.13.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64=
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4/go.mod h1:T9YF2M40nIgbVgp3rreNmTged+9HrbNTIQf1PsaIiTA= github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4/go.mod h1:T9YF2M40nIgbVgp3rreNmTged+9HrbNTIQf1PsaIiTA=
github.com/flosch/pongo2 v0.0.0-20200529170236-5abacdfa4915 h1:rNVrewdFbSujcoKZifC6cHJfqCTbCIR7XTLHW5TqUWU= github.com/flosch/pongo2 v0.0.0-20200529170236-5abacdfa4915 h1:rNVrewdFbSujcoKZifC6cHJfqCTbCIR7XTLHW5TqUWU=
@ -95,6 +102,8 @@ github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/glendc/gopher-json v0.0.0-20170414221815-dc4743023d0c/go.mod h1:Gja1A+xZ9BoviGJNA2E9vFkPjjsl+CoJxSXiQM1UXtw= github.com/glendc/gopher-json v0.0.0-20170414221815-dc4743023d0c/go.mod h1:Gja1A+xZ9BoviGJNA2E9vFkPjjsl+CoJxSXiQM1UXtw=
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
github.com/go-chi/chi v4.0.0+incompatible h1:SiLLEDyAkqNnw+T/uDTf3aFB9T4FTrwMpuYrgaRcnW4=
github.com/go-chi/chi v4.0.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@ -116,6 +125,8 @@ github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gobuffalo/envy v1.9.0 h1:eZR0DuEgVLfeIb1zIKt3bT4YovIMf9O9LXQeCZLXpqE=
github.com/gobuffalo/envy v1.9.0/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@ -186,6 +197,8 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@ -235,6 +248,13 @@ github.com/lithammer/shortuuid/v3 v3.0.4 h1:uj4xhotfY92Y1Oa6n6HUiFn87CdoEHYUlTy0
github.com/lithammer/shortuuid/v3 v3.0.4/go.mod h1:RviRjexKqIzx/7r1peoAITm6m7gnif/h+0zmolKJjzw= github.com/lithammer/shortuuid/v3 v3.0.4/go.mod h1:RviRjexKqIzx/7r1peoAITm6m7gnif/h+0zmolKJjzw=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailgun/mailgun-go v1.1.1 h1:mjMcm4qz+SbjAYbGJ6DKROViKtO5S0YjpuOUxQfdr2A=
github.com/mailgun/mailgun-go v2.0.0+incompatible h1:0FoRHWwMUctnd8KIR3vtZbqdfjpIMxOZgcSa51s8F8o=
github.com/mailgun/mailgun-go v2.0.0+incompatible/go.mod h1:NWTyU+O4aczg/nsGhQnvHL6v2n5Gy6Sv5tNDVvC6FbU=
github.com/mailgun/mailgun-go/v4 v4.1.3 h1:KLa5EZaOMMeyvY/lfAhWxv9ealB3mtUsMz0O9XmTtP0=
github.com/mailgun/mailgun-go/v4 v4.1.3/go.mod h1:R9kHUQBptF4iSEjhriCQizplCDwrnDShy8w/iPiOfaM=
github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM=
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
github.com/mattn/go-colorable v0.0.0-20170327083344-ded68f7a9561/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.0.0-20170327083344-ded68f7a9561/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
@ -321,6 +341,8 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.2 h1:XU784Pr0wdahMY2bYcyK6N1KuaRAdLtqD4qd8D18Bfs=
github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=

View File

@ -68,7 +68,7 @@ func (jf *Jellyfin) init(server, client, version, device, deviceId string) error
return nil return nil
} }
func (jf *Jellyfin) authenticate(username, password string) (int, error) { func (jf *Jellyfin) authenticate(username, password string) (map[string]interface{}, int, error) {
jf.username = username jf.username = username
jf.password = password jf.password = password
jf.loginParams = map[string]string{ jf.loginParams = map[string]string{
@ -83,7 +83,7 @@ func (jf *Jellyfin) authenticate(username, password string) (int, error) {
} }
resp, err := jf.httpClient.Do(req) resp, err := jf.httpClient.Do(req)
if err != nil || resp.StatusCode != 200 { if err != nil || resp.StatusCode != 200 {
return resp.StatusCode, err return nil, resp.StatusCode, err
} }
defer resp.Body.Close() defer resp.Body.Close()
var data io.Reader var data io.Reader
@ -96,11 +96,12 @@ func (jf *Jellyfin) authenticate(username, password string) (int, error) {
var respData map[string]interface{} var respData map[string]interface{}
json.NewDecoder(data).Decode(&respData) json.NewDecoder(data).Decode(&respData)
jf.accessToken = respData["AccessToken"].(string) jf.accessToken = respData["AccessToken"].(string)
user := respData["User"].(map[string]interface{})
jf.userId = respData["User"].(map[string]interface{})["Id"].(string) jf.userId = respData["User"].(map[string]interface{})["Id"].(string)
jf.auth = fmt.Sprintf("MediaBrowser Client=%s, Device=%s, DeviceId=%s, Version=%s, Token=%s", jf.client, jf.device, jf.deviceId, jf.version, jf.accessToken) jf.auth = fmt.Sprintf("MediaBrowser Client=%s, Device=%s, DeviceId=%s, Version=%s, Token=%s", jf.client, jf.device, jf.deviceId, jf.version, jf.accessToken)
jf.header["X-Emby-Authorization"] = jf.auth jf.header["X-Emby-Authorization"] = jf.auth
jf.authenticated = true jf.authenticated = true
return resp.StatusCode, nil return user, resp.StatusCode, nil
} }
func (jf *Jellyfin) _getReader(url string, params map[string]string) (io.Reader, int, error) { func (jf *Jellyfin) _getReader(url string, params map[string]string) (io.Reader, int, error) {
@ -118,7 +119,7 @@ func (jf *Jellyfin) _getReader(url string, params map[string]string) (io.Reader,
if err != nil || resp.StatusCode != 200 { if err != nil || resp.StatusCode != 200 {
if resp.StatusCode == 401 && jf.authenticated { if resp.StatusCode == 401 && jf.authenticated {
jf.authenticated = false jf.authenticated = false
_, authErr := jf.authenticate(jf.username, jf.password) _, _, authErr := jf.authenticate(jf.username, jf.password)
if authErr == nil { if authErr == nil {
v1, v2, v3 := jf._getReader(url, params) v1, v2, v3 := jf._getReader(url, params)
return v1, v2, v3 return v1, v2, v3
@ -149,7 +150,7 @@ func (jf *Jellyfin) _post(url string, data map[string]interface{}, response bool
if err != nil || resp.StatusCode != 200 { if err != nil || resp.StatusCode != 200 {
if resp.StatusCode == 401 && jf.authenticated { if resp.StatusCode == 401 && jf.authenticated {
jf.authenticated = false jf.authenticated = false
_, authErr := jf.authenticate(jf.username, jf.password) _, _, authErr := jf.authenticate(jf.username, jf.password)
if authErr == nil { if authErr == nil {
v1, v2, v3 := jf._post(url, data, response) v1, v2, v3 := jf._post(url, data, response)
return v1, v2, v3 return v1, v2, v3

View File

@ -34,6 +34,7 @@ type appContext struct {
timePattern string timePattern string
storage Storage storage Storage
validator Validator validator Validator
email Emailer
} }
func GenerateSecret(length int) (string, error) { func GenerateSecret(length int) (string, error) {
@ -126,20 +127,26 @@ func main() {
} }
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
} }
} }
ctx.validator.init(validatorConf) ctx.validator.init(validatorConf)
ctx.email.init(ctx)
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.LoadHTMLGlob("data/templates/*") router.LoadHTMLGlob("data/templates/*")
router.GET("/", ctx.AdminPage) router.GET("/", ctx.AdminPage)
router.GET("/getToken", ctx.GetToken) router.GET("/getToken", ctx.GetToken)
router.POST("/newUser", ctx.NewUser)
router.GET("/invite/:invCode", ctx.InviteProxy)
api := router.Group("/", ctx.webAuth()) api := router.Group("/", ctx.webAuth())
api.POST("/generateInvite", ctx.GenerateInvite) api.POST("/generateInvite", ctx.GenerateInvite)
api.GET("/getInvites", ctx.GetInvites) api.GET("/getInvites", ctx.GetInvites)
api.POST("/setNotify", ctx.SetNotify)
router.Run(":8080") router.Run(":8080")
} }

View File

@ -20,8 +20,8 @@ func (vd *Validator) init(criteria ValidatorConf) {
} }
func (vd *Validator) validate(password string) map[string]bool { func (vd *Validator) validate(password string) map[string]bool {
count := vd.criteria count := map[string]int{}
for key, _ := range count { for key, _ := range vd.criteria {
count[key] = 0 count[key] = 0
} }
for _, c := range password { for _, c := range password {
@ -40,21 +40,24 @@ func (vd *Validator) validate(password string) map[string]bool {
} }
} }
} }
var results map[string]bool fmt.Println(count)
results := map[string]bool{}
for criterion, num := range count { for criterion, num := range count {
results[criterion] = true
if num < vd.criteria[criterion] { if num < vd.criteria[criterion] {
results[criterion] = false results[criterion] = false
} else {
results[criterion] = true
} }
} }
fmt.Println(results)
return results return results
} }
func (vd *Validator) getCriteria() map[string]string { func (vd *Validator) getCriteria() map[string]string {
var lines map[string]string lines := map[string]string{}
for criterion, min := range vd.criteria { for criterion, min := range vd.criteria {
if min > 0 { if min > 0 {
text := fmt.Sprintf("Must have at least %d", min) text := fmt.Sprintf("Must have at least %d ", min)
if min == 1 { if min == 1 {
text += strings.TrimSuffix(criterion, "s") text += strings.TrimSuffix(criterion, "s")
} else { } else {

View File

@ -2,7 +2,6 @@ package main
import ( import (
"encoding/json" "encoding/json"
"fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"time" "time"
@ -18,7 +17,7 @@ type Storage struct {
// timePattern: %Y-%m-%dT%H:%M:%S.%f // timePattern: %Y-%m-%dT%H:%M:%S.%f
type Invite struct { type Invite struct {
Created string `json:"created"` Created time.Time `json:"created"`
NoLimit bool `json:"no-limit"` NoLimit bool `json:"no-limit"`
RemainingUses int `json:"remaining-uses"` RemainingUses int `json:"remaining-uses"`
ValidTill time.Time `json:"valid_till"` ValidTill time.Time `json:"valid_till"`
@ -34,7 +33,6 @@ func (st *Storage) loadInvites() error {
} }
func (st *Storage) storeInvites() error { func (st *Storage) storeInvites() error {
fmt.Println("INVITES:", st.invites)
return storeJSON(st.invite_path, st.invites) return storeJSON(st.invite_path, st.invites)
} }
@ -80,12 +78,9 @@ func loadJSON(path string, obj interface{}) error {
} }
func storeJSON(path string, obj interface{}) error { func storeJSON(path string, obj interface{}) error {
fmt.Println("OBJ:", obj)
test := json.NewEncoder(os.Stdout) test := json.NewEncoder(os.Stdout)
test.Encode(obj) test.Encode(obj)
data, err := json.Marshal(obj) data, err := json.Marshal(obj)
fmt.Println("DATA:", string(data))
fmt.Println("ERR:", err)
if err != nil { if err != nil {
return err return err
} }

View File

@ -6,7 +6,7 @@ import (
) )
func (ctx *appContext) AdminPage(gc *gin.Context) { func (ctx *appContext) AdminPage(gc *gin.Context) {
bs5, _ := ctx.config.Section("ui").Key("bs5").Bool() bs5 := ctx.config.Section("ui").Key("bs5").MustBool(false)
emailEnabled, _ := ctx.config.Section("invite_emails").Key("enabled").Bool() emailEnabled, _ := ctx.config.Section("invite_emails").Key("enabled").Bool()
notificationsEnabled, _ := ctx.config.Section("notifications").Key("enabled").Bool() notificationsEnabled, _ := ctx.config.Section("notifications").Key("enabled").Bool()
gc.HTML(http.StatusOK, "admin.html", gin.H{ gc.HTML(http.StatusOK, "admin.html", gin.H{
@ -17,3 +17,24 @@ func (ctx *appContext) AdminPage(gc *gin.Context) {
"notifications": notificationsEnabled, "notifications": notificationsEnabled,
}) })
} }
func (ctx *appContext) InviteProxy(gc *gin.Context) {
code := gc.Param("invCode")
if ctx.checkInvite(code, false, "") {
email := ctx.storage.invites[code].Email
gc.HTML(http.StatusOK, "form.html", gin.H{
"bs5": ctx.config.Section("ui").Key("bs5").MustBool(false),
"cssFile": ctx.cssFile,
"contactMessage": ctx.config.Section("ui").Key("contac_message").String(),
"helpMessage": ctx.config.Section("ui").Key("help_message").String(),
"successMessage": ctx.config.Section("ui").Key("success_message").String(),
"jfLink": ctx.config.Section("jellyfin").Key("public_server").String(),
"validate": ctx.config.Section("password_validation").Key("enabled").MustBool(false),
"requirements": ctx.validator.getCriteria(),
"email": email,
"username": !ctx.config.Section("email").Key("no_username").MustBool(false),
})
} else {
respond(401, "Invalid code", gc)
}
}