mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-22 09:00:10 +00:00
Functioning user creation, notifications,
Fixed password validation for new users, add invite route, couple other fixes.
This commit is contained in:
parent
d8fb6e5613
commit
961b9afa75
144
api.go
144
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()
|
||||
}
|
||||
}
|
||||
|
28
auth.go
28
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 {
|
||||
|
@ -152,7 +152,7 @@
|
||||
<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);">
|
||||
<h3>User Created</h3>
|
||||
<p>A user was created using code {{ code }}.</p>
|
||||
<p>A user was created using code {{ .code }}.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -165,9 +165,9 @@
|
||||
<th>Time</th>
|
||||
</tr>
|
||||
<tr style="font-style: italic; text-align: left; color: rgb(153,153,153);">
|
||||
<th>{{ username }}</th>
|
||||
<th>{{ address }}</th>
|
||||
<th>{{ time }}</th>
|
||||
<th>{{ .username }}</th>
|
||||
<th>{{ .address }}</th>
|
||||
<th>{{ .time }}</th>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
@ -239,4 +239,4 @@
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
@ -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.
|
||||
|
@ -151,10 +151,10 @@
|
||||
<tr>
|
||||
<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);">
|
||||
<p>Hi {{ username }},</p>
|
||||
<p>Hi {{ .username }},</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>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>
|
||||
</div>
|
||||
</td>
|
||||
@ -165,7 +165,7 @@
|
||||
<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">
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
@ -215,7 +215,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
</table>
|
||||
@ -240,4 +240,4 @@
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
@ -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 }}
|
||||
|
@ -152,7 +152,7 @@
|
||||
<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);">
|
||||
<h3>Invite Expired.</h3>
|
||||
<p>Code {{ code }} expired at {{ expiry }}.</p>
|
||||
<p>Code {{ .code }} expired at {{ .expiry }}.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -224,4 +224,4 @@
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
@ -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.
|
||||
|
@ -154,7 +154,7 @@
|
||||
<p>Hi,</p>
|
||||
<h3>You've been invited to Jellyfin.</h3>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
@ -163,7 +163,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||
<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">
|
||||
<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>
|
||||
</tr>
|
||||
</table>
|
||||
@ -212,7 +212,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
</table>
|
||||
@ -237,4 +237,4 @@
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
@ -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 }}
|
||||
|
@ -13,16 +13,16 @@
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link rel="stylesheet" type="text/css" href="{{ css_file }}">
|
||||
{% if not bs5 %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ .cssFile }}">
|
||||
{{ 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>
|
||||
{% 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>
|
||||
{% 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>
|
||||
{% 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>
|
||||
{% endif %}
|
||||
{{ end }}
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<style>
|
||||
.pageContainer {
|
||||
@ -51,10 +51,10 @@
|
||||
<h5 class="modal-title" id="successTitle">Success!</h5>
|
||||
</div>
|
||||
<div class="modal-body" id="successBody">
|
||||
<p>{{ successMessage }}</p>
|
||||
<p>{{ .successMessage }}</p>
|
||||
</div>
|
||||
<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>
|
||||
@ -63,8 +63,8 @@
|
||||
<h1>
|
||||
Create Account
|
||||
</h1>
|
||||
<p>{{ helpMessage }}</p>
|
||||
<p class="contactBox">{{ contactMessage }}</p>
|
||||
<p>{{ .helpMessage }}</p>
|
||||
<p class="contactBox">{{ .contactMessage }}</p>
|
||||
<div class="container" id="container">
|
||||
<div class="row" id="cardContainer">
|
||||
<div class="col-sm">
|
||||
@ -74,14 +74,14 @@
|
||||
<form action="#" method="POST" id="accountForm">
|
||||
<div class="form-group">
|
||||
<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>
|
||||
{% if username %}
|
||||
{{ if .username }}
|
||||
<div class="form-group">
|
||||
<label for="inputUsername">Username</label>
|
||||
<input type="username" class="form-control" id="inputUsername" name="username" placeholder="Username" required>
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ end }}
|
||||
<div class="form-group">
|
||||
<label for="inputPassword">Password</label>
|
||||
<input type="password" class="form-control" id="inputPassword" name="password" placeholder="Password" required>
|
||||
@ -95,32 +95,32 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if validate %}
|
||||
{{ if .validate }}
|
||||
<div class="col-sm" id="requirementBox">
|
||||
<div class="card mb-3 requirementBox">
|
||||
<div class="card-header">Password Requirements</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-group">
|
||||
{% for key, value in requirements.items() %}
|
||||
<li id="{{ key }}" class="list-group-item list-group-item-danger">
|
||||
<div> {{ value }}</div>
|
||||
{{ range $key, $value := .requirements }}
|
||||
<li id="{{ $key }}" class="list-group-item list-group-item-danger">
|
||||
<div> {{ $value }}</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="serialize.js"></script>
|
||||
<script>
|
||||
{% if bs5 %}
|
||||
{{ if .bs5 }}
|
||||
var bsVersion = 5;
|
||||
{% else %}
|
||||
{{ else }}
|
||||
var bsVersion = 4;
|
||||
{% endif %}
|
||||
{{ end }}
|
||||
if (bsVersion == 5) {
|
||||
var successBox = new bootstrap.Modal(document.getElementById('successBox'));
|
||||
} else if (bsVersion == 4) {
|
||||
@ -162,9 +162,9 @@
|
||||
toggleSpinner();
|
||||
var send = serializeForm('accountForm');
|
||||
send['code'] = code;
|
||||
{% if not username %}
|
||||
{{ if not .username }}
|
||||
send['email'] = send['username'];
|
||||
{% endif %}
|
||||
{{ end }}
|
||||
send = JSON.stringify(send);
|
||||
var req = new XMLHttpRequest();
|
||||
req.open("POST", "/newUser", true);
|
209
email.go
Normal file
209
email.go
Normal 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
5
go.mod
@ -6,16 +6,21 @@ require (
|
||||
github.com/astaxie/beego v1.12.2
|
||||
github.com/beego/bee v1.12.0 // indirect
|
||||
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/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2
|
||||
github.com/gin-gonic/gin v1.6.3
|
||||
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/knz/strtime v0.0.0-20200318182718-be999391ffa9
|
||||
github.com/labstack/echo/v4 v4.1.16
|
||||
github.com/lestrrat-go/strftime v1.0.3
|
||||
github.com/lib/pq v1.7.1 // indirect
|
||||
github.com/lithammer/shortuuid/v3 v3.0.4 // indirect
|
||||
github.com/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-runewidth v0.0.9 // indirect
|
||||
github.com/mitchellh/mapstructure v1.3.3 // indirect
|
||||
|
22
go.sum
22
go.sum
@ -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/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/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/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/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4/go.mod h1:T9YF2M40nIgbVgp3rreNmTged+9HrbNTIQf1PsaIiTA=
|
||||
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/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-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-kit/kit v0.8.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/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
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.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
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/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
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/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||
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.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
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/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.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/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=
|
||||
|
11
jfapi.go
11
jfapi.go
@ -68,7 +68,7 @@ func (jf *Jellyfin) init(server, client, version, device, deviceId string) error
|
||||
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.password = password
|
||||
jf.loginParams = map[string]string{
|
||||
@ -83,7 +83,7 @@ func (jf *Jellyfin) authenticate(username, password string) (int, error) {
|
||||
}
|
||||
resp, err := jf.httpClient.Do(req)
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
return resp.StatusCode, err
|
||||
return nil, resp.StatusCode, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var data io.Reader
|
||||
@ -96,11 +96,12 @@ func (jf *Jellyfin) authenticate(username, password string) (int, error) {
|
||||
var respData map[string]interface{}
|
||||
json.NewDecoder(data).Decode(&respData)
|
||||
jf.accessToken = respData["AccessToken"].(string)
|
||||
user := respData["User"].(map[string]interface{})
|
||||
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.header["X-Emby-Authorization"] = jf.auth
|
||||
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) {
|
||||
@ -118,7 +119,7 @@ func (jf *Jellyfin) _getReader(url string, params map[string]string) (io.Reader,
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
if resp.StatusCode == 401 && jf.authenticated {
|
||||
jf.authenticated = false
|
||||
_, authErr := jf.authenticate(jf.username, jf.password)
|
||||
_, _, authErr := jf.authenticate(jf.username, jf.password)
|
||||
if authErr == nil {
|
||||
v1, v2, v3 := jf._getReader(url, params)
|
||||
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 resp.StatusCode == 401 && jf.authenticated {
|
||||
jf.authenticated = false
|
||||
_, authErr := jf.authenticate(jf.username, jf.password)
|
||||
_, _, authErr := jf.authenticate(jf.username, jf.password)
|
||||
if authErr == nil {
|
||||
v1, v2, v3 := jf._post(url, data, response)
|
||||
return v1, v2, v3
|
||||
|
9
main.go
9
main.go
@ -34,6 +34,7 @@ type appContext struct {
|
||||
timePattern string
|
||||
storage Storage
|
||||
validator Validator
|
||||
email Emailer
|
||||
}
|
||||
|
||||
func GenerateSecret(length int) (string, error) {
|
||||
@ -126,20 +127,26 @@ func main() {
|
||||
}
|
||||
|
||||
if !ctx.config.Section("password_validation").Key("enabled").MustBool(false) {
|
||||
for key, _ := range validatorConf {
|
||||
for key := range validatorConf {
|
||||
validatorConf[key] = 0
|
||||
}
|
||||
}
|
||||
ctx.validator.init(validatorConf)
|
||||
|
||||
ctx.email.init(ctx)
|
||||
|
||||
router := gin.Default()
|
||||
router.Use(static.Serve("/", static.LocalFile("data/static", false)))
|
||||
router.Use(static.Serve("/invite/", static.LocalFile("data/static", false)))
|
||||
router.LoadHTMLGlob("data/templates/*")
|
||||
router.GET("/", ctx.AdminPage)
|
||||
router.GET("/getToken", ctx.GetToken)
|
||||
router.POST("/newUser", ctx.NewUser)
|
||||
router.GET("/invite/:invCode", ctx.InviteProxy)
|
||||
api := router.Group("/", ctx.webAuth())
|
||||
api.POST("/generateInvite", ctx.GenerateInvite)
|
||||
api.GET("/getInvites", ctx.GetInvites)
|
||||
api.POST("/setNotify", ctx.SetNotify)
|
||||
|
||||
router.Run(":8080")
|
||||
}
|
||||
|
15
pwval.go
15
pwval.go
@ -20,8 +20,8 @@ func (vd *Validator) init(criteria ValidatorConf) {
|
||||
}
|
||||
|
||||
func (vd *Validator) validate(password string) map[string]bool {
|
||||
count := vd.criteria
|
||||
for key, _ := range count {
|
||||
count := map[string]int{}
|
||||
for key, _ := range vd.criteria {
|
||||
count[key] = 0
|
||||
}
|
||||
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 {
|
||||
results[criterion] = true
|
||||
if num < vd.criteria[criterion] {
|
||||
results[criterion] = false
|
||||
} else {
|
||||
results[criterion] = true
|
||||
}
|
||||
}
|
||||
fmt.Println(results)
|
||||
return results
|
||||
}
|
||||
|
||||
func (vd *Validator) getCriteria() map[string]string {
|
||||
var lines map[string]string
|
||||
lines := map[string]string{}
|
||||
for criterion, min := range vd.criteria {
|
||||
if min > 0 {
|
||||
text := fmt.Sprintf("Must have at least %d", min)
|
||||
text := fmt.Sprintf("Must have at least %d ", min)
|
||||
if min == 1 {
|
||||
text += strings.TrimSuffix(criterion, "s")
|
||||
} else {
|
||||
|
@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
@ -18,7 +17,7 @@ type Storage struct {
|
||||
// timePattern: %Y-%m-%dT%H:%M:%S.%f
|
||||
|
||||
type Invite struct {
|
||||
Created string `json:"created"`
|
||||
Created time.Time `json:"created"`
|
||||
NoLimit bool `json:"no-limit"`
|
||||
RemainingUses int `json:"remaining-uses"`
|
||||
ValidTill time.Time `json:"valid_till"`
|
||||
@ -34,7 +33,6 @@ func (st *Storage) loadInvites() error {
|
||||
}
|
||||
|
||||
func (st *Storage) storeInvites() error {
|
||||
fmt.Println("INVITES:", 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 {
|
||||
fmt.Println("OBJ:", obj)
|
||||
test := json.NewEncoder(os.Stdout)
|
||||
test.Encode(obj)
|
||||
data, err := json.Marshal(obj)
|
||||
fmt.Println("DATA:", string(data))
|
||||
fmt.Println("ERR:", err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
23
views.go
23
views.go
@ -6,7 +6,7 @@ import (
|
||||
)
|
||||
|
||||
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()
|
||||
notificationsEnabled, _ := ctx.config.Section("notifications").Key("enabled").Bool()
|
||||
gc.HTML(http.StatusOK, "admin.html", gin.H{
|
||||
@ -17,3 +17,24 @@ func (ctx *appContext) AdminPage(gc *gin.Context) {
|
||||
"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)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user