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

144
api.go
View File

@ -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
View File

@ -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 {

View File

@ -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>

View File

@ -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.

View File

@ -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>

View File

@ -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 }}

View File

@ -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>

View File

@ -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.

View File

@ -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>

View File

@ -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 }}

View File

@ -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
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/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
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/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=

View File

@ -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

View File

@ -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")
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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)
}
}