initial public commit

This commit is contained in:
Gani Georgiev
2022-07-07 00:19:05 +03:00
commit 3d07f0211d
484 changed files with 92412 additions and 0 deletions
+76
View File
@@ -0,0 +1,76 @@
package mails
import (
"fmt"
"net/mail"
"strings"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/mails/templates"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/tokens"
)
// SendAdminPasswordReset sends a password reset request email to the specified admin.
func SendAdminPasswordReset(app core.App, admin *models.Admin) error {
token, tokenErr := tokens.NewAdminResetPasswordToken(app, admin)
if tokenErr != nil {
return tokenErr
}
actionUrl, urlErr := normalizeUrl(fmt.Sprintf(
"%s/#/confirm-password-reset/%s",
strings.TrimSuffix(app.Settings().Meta.AppUrl, "/"),
token,
))
if urlErr != nil {
return urlErr
}
params := struct {
AppName string
AppUrl string
Admin *models.Admin
Token string
ActionUrl string
}{
AppName: app.Settings().Meta.AppName,
AppUrl: app.Settings().Meta.AppUrl,
Admin: admin,
Token: token,
ActionUrl: actionUrl,
}
mailClient := app.NewMailClient()
event := &core.MailerAdminEvent{
MailClient: mailClient,
Admin: admin,
Meta: map[string]any{"token": token},
}
sendErr := app.OnMailerBeforeAdminResetPasswordSend().Trigger(event, func(e *core.MailerAdminEvent) error {
// resolve body template
body, renderErr := resolveTemplateContent(params, templates.Layout, templates.AdminPasswordResetBody)
if renderErr != nil {
return renderErr
}
return e.MailClient.Send(
mail.Address{
Name: app.Settings().Meta.SenderName,
Address: app.Settings().Meta.SenderAddress,
},
mail.Address{Address: e.Admin.Email},
"Reset admin password",
body,
nil,
)
})
if sendErr == nil {
app.OnMailerAfterAdminResetPasswordSend().Trigger(event)
}
return sendErr
}
+37
View File
@@ -0,0 +1,37 @@
package mails_test
import (
"strings"
"testing"
"github.com/pocketbase/pocketbase/mails"
"github.com/pocketbase/pocketbase/tests"
)
func TestSendAdminPasswordReset(t *testing.T) {
testApp, _ := tests.NewTestApp()
defer testApp.Cleanup()
// ensure that action url normalization will be applied
testApp.Settings().Meta.AppUrl = "http://localhost:8090////"
admin, _ := testApp.Dao().FindAdminByEmail("test@example.com")
err := mails.SendAdminPasswordReset(testApp, admin)
if err != nil {
t.Fatal(err)
}
if testApp.TestMailer.TotalSend != 1 {
t.Fatalf("Expected one email to be sent, got %d", testApp.TestMailer.TotalSend)
}
expectedParts := []string{
"http://localhost:8090/#/confirm-password-reset/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.",
}
for _, part := range expectedParts {
if !strings.Contains(testApp.TestMailer.LastHtmlBody, part) {
t.Fatalf("Couldn't find %s \nin\n %s", part, testApp.TestMailer.LastHtmlBody)
}
}
}
+58
View File
@@ -0,0 +1,58 @@
// Package mails implements various helper methods for sending user and admin
// emails like forgotten password, verification, etc.
package mails
import (
"bytes"
"net/url"
"path"
"strings"
"text/template"
)
// normalizeUrl removes duplicated slashes from a url path.
func normalizeUrl(originalUrl string) (string, error) {
u, err := url.Parse(originalUrl)
if err != nil {
return "", err
}
hasSlash := strings.HasSuffix(u.Path, "/")
// clean up path by removing duplicated /
u.Path = path.Clean(u.Path)
u.RawPath = path.Clean(u.RawPath)
// restore original trailing slash
if hasSlash && !strings.HasSuffix(u.Path, "/") {
u.Path += "/"
u.RawPath += "/"
}
return u.String(), nil
}
// resolveTemplateContent resolves inline html template strings.
func resolveTemplateContent(data any, content ...string) (string, error) {
if len(content) == 0 {
return "", nil
}
t := template.New("inline_template")
var parseErr error
for _, v := range content {
t, parseErr = t.Parse(v)
if parseErr != nil {
return "", parseErr
}
}
var wr bytes.Buffer
if executeErr := t.Execute(&wr, data); executeErr != nil {
return "", executeErr
}
return wr.String(), nil
}
+25
View File
@@ -0,0 +1,25 @@
package templates
// Available variables:
//
// ```
// Admin *models.Admin
// AppName string
// AppUrl string
// Token string
// ActionUrl string
// ```
const AdminPasswordResetBody = `
{{define "content"}}
<p>Hello,</p>
<p>Follow this link to reset your admin password for {{.AppName}}.</p>
<p>
<a class="btn" href="{{.ActionUrl}}">Reset password</a>
<a class="fallback-link" href="{{.ActionUrl}}">{{.ActionUrl}}</a>
</p>
<p><i>If you did not request to reset your password, please ignore this email and the link will expire on its own.</i></p>
{{end}}
`
+8
View File
@@ -0,0 +1,8 @@
package templates
// Available variables:
//
// ```
// HtmlContent template.HTML
// ```
const HtmlBody = `{{define "content"}}{{.HtmlContent}}{{end}}`
+117
View File
@@ -0,0 +1,117 @@
package templates
const Layout = `
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<style>
body, html {
padding: 0;
margin: 0;
border: 0;
color: #16161a;
background: #fff;
font-size: 14px;
line-height: 20px;
font-weight: normal;
font-family: Source Sans Pro, sans-serif, emoji;
}
body {
padding: 30px 20px;
}
p {
display: block;
margin: 10px 0;
font-family: Source Sans Pro, sans-serif, emoji;
}
strong {
font-weight: bold;
}
em, i {
font-style: italic;
}
small {
font-size: 12px;
line-height: 16px;
}
hr {
display: block;
height: 1px;
border: 0;
width: 100%;
background: #e1e6ea;
margin: 10px 0;
}
a {
color: inherit;
}
.hidden {
display: none !important;
}
.fallback-link {
display: none !important;
word-break: break-all;
font-size: 11px;
color: #666f75;
}
.btn {
display: inline-block;
vertical-align: top;
border: 0;
cursor: pointer;
color: #fff !important;
background: #16161a !important;
text-decoration: none !important;
line-height: 45px;
width: 100%;
max-width: 100%;
text-align: center;
padding: 0 30px;
margin: 10px 0;
font-family: Source Sans Pro, sans-serif, emoji;;
font-size: 14px;
font-weight: bold;
border-radius: 3px;
box-sizing: border-box;
}
.wrapper {
display: block;
width: 460px;
max-width: 100%;
margin: auto;
font-family: inherit;
}
.content {
display: block;
width: 100%;
padding: 10px 20px;
font-family: inherit;
box-sizing: border-box;
background: #fff;
border-radius: 10px;
-webkit-box-shadow: 0px 2px 30px 0px rgb(0,0,0,0.05);
-moz-box-shadow: 0px 2px 30px 0px rgb(0,0,0,0.05);
box-shadow: 0px 2px 30px 0px rgb(0,0,0,0.05);
}
.footer {
display: block;
width: 100%;
text-align: center;
margin: 10px 0;
color: #666f75;
font-size: 11px;
font-family: inherit;
}
</style>
</head>
<body>
<div class="wrapper">
<div class="content">
{{template "content" .}}
</div>
</div>
</body>
</html>
`
@@ -0,0 +1,26 @@
package templates
// Available variables:
//
// ```
// User *models.User
// AppName string
// AppUrl string
// Token string
// ActionUrl string
// ```
const UserConfirmEmailChangeBody = `
{{define "content"}}
<p>Hello,</p>
<p>Click on the button below to confirm your new email address.</p>
<p>
<a class="btn" href="{{.ActionUrl}}">Confirm new email</a>
<a class="fallback-link" href="{{.ActionUrl}}">{{.ActionUrl}}</a>
</p>
<p><i>If you didnt ask to change your email address, you can ignore this email.</i></p>
<p>
Thanks,<br/>
{{.AppName}} team
</p>
{{end}}
`
+26
View File
@@ -0,0 +1,26 @@
package templates
// Available variables:
//
// ```
// User *models.User
// AppName string
// AppUrl string
// Token string
// ActionUrl string
// ```
const UserPasswordResetBody = `
{{define "content"}}
<p>Hello,</p>
<p>Click on the button below to reset your password.</p>
<p>
<a class="btn" href="{{.ActionUrl}}">Reset password</a>
<a class="fallback-link" href="{{.ActionUrl}}">{{.ActionUrl}}</a>
</p>
<p><i>If you didnt ask to reset your password, you can ignore this email.</i></p>
<p>
Thanks,<br/>
{{.AppName}} team
</p>
{{end}}
`
+26
View File
@@ -0,0 +1,26 @@
package templates
// Available variables:
//
// ```
// User *models.User
// AppName string
// AppUrl string
// Token string
// ActionUrl string
// ```
const UserVerificationBody = `
{{define "content"}}
<p>Hello,</p>
<p>Thank you for joining us at {{.AppName}}.</p>
<p>Click on the button below to verify your email address.</p>
<p>
<a class="btn" href="{{.ActionUrl}}">Verify</a>
<a class="fallback-link" href="{{.ActionUrl}}">{{.ActionUrl}}</a>
</p>
<p>
Thanks,<br/>
{{.AppName}} team
</p>
{{end}}
`
+192
View File
@@ -0,0 +1,192 @@
package mails
import (
"net/mail"
"strings"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/mails/templates"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/tokens"
)
func prepareUserEmailBody(
app core.App,
user *models.User,
token string,
actionUrl string,
bodyTemplate string,
) (string, error) {
settings := app.Settings()
// replace action url placeholder params (if any)
actionUrlParams := map[string]string{
core.EmailPlaceholderAppUrl: settings.Meta.AppUrl,
core.EmailPlaceholderToken: token,
}
for k, v := range actionUrlParams {
actionUrl = strings.ReplaceAll(actionUrl, k, v)
}
var urlErr error
actionUrl, urlErr = normalizeUrl(actionUrl)
if urlErr != nil {
return "", urlErr
}
params := struct {
AppName string
AppUrl string
User *models.User
Token string
ActionUrl string
}{
AppName: settings.Meta.AppName,
AppUrl: settings.Meta.AppUrl,
User: user,
Token: token,
ActionUrl: actionUrl,
}
return resolveTemplateContent(params, templates.Layout, bodyTemplate)
}
// SendUserPasswordReset sends a password reset request email to the specified user.
func SendUserPasswordReset(app core.App, user *models.User) error {
token, tokenErr := tokens.NewUserResetPasswordToken(app, user)
if tokenErr != nil {
return tokenErr
}
mailClient := app.NewMailClient()
event := &core.MailerUserEvent{
MailClient: mailClient,
User: user,
Meta: map[string]any{"token": token},
}
sendErr := app.OnMailerBeforeUserResetPasswordSend().Trigger(event, func(e *core.MailerUserEvent) error {
body, err := prepareUserEmailBody(
app,
user,
token,
app.Settings().Meta.UserResetPasswordUrl,
templates.UserPasswordResetBody,
)
if err != nil {
return err
}
return e.MailClient.Send(
mail.Address{
Name: app.Settings().Meta.SenderName,
Address: app.Settings().Meta.SenderAddress,
},
mail.Address{Address: e.User.Email},
("Reset your " + app.Settings().Meta.AppName + " password"),
body,
nil,
)
})
if sendErr == nil {
app.OnMailerAfterUserResetPasswordSend().Trigger(event)
}
return sendErr
}
// SendUserVerification sends a verification request email to the specified user.
func SendUserVerification(app core.App, user *models.User) error {
token, tokenErr := tokens.NewUserVerifyToken(app, user)
if tokenErr != nil {
return tokenErr
}
mailClient := app.NewMailClient()
event := &core.MailerUserEvent{
MailClient: mailClient,
User: user,
Meta: map[string]any{"token": token},
}
sendErr := app.OnMailerBeforeUserVerificationSend().Trigger(event, func(e *core.MailerUserEvent) error {
body, err := prepareUserEmailBody(
app,
user,
token,
app.Settings().Meta.UserVerificationUrl,
templates.UserVerificationBody,
)
if err != nil {
return err
}
return e.MailClient.Send(
mail.Address{
Name: app.Settings().Meta.SenderName,
Address: app.Settings().Meta.SenderAddress,
},
mail.Address{Address: e.User.Email},
("Verify your " + app.Settings().Meta.AppName + " email"),
body,
nil,
)
})
if sendErr == nil {
app.OnMailerAfterUserVerificationSend().Trigger(event)
}
return sendErr
}
// SendUserChangeEmail sends a change email confirmation email to the specified user.
func SendUserChangeEmail(app core.App, user *models.User, newEmail string) error {
token, tokenErr := tokens.NewUserChangeEmailToken(app, user, newEmail)
if tokenErr != nil {
return tokenErr
}
mailClient := app.NewMailClient()
event := &core.MailerUserEvent{
MailClient: mailClient,
User: user,
Meta: map[string]any{
"token": token,
"newEmail": newEmail,
},
}
sendErr := app.OnMailerBeforeUserChangeEmailSend().Trigger(event, func(e *core.MailerUserEvent) error {
body, err := prepareUserEmailBody(
app,
user,
token,
app.Settings().Meta.UserConfirmEmailChangeUrl,
templates.UserConfirmEmailChangeBody,
)
if err != nil {
return err
}
return e.MailClient.Send(
mail.Address{
Name: app.Settings().Meta.SenderName,
Address: app.Settings().Meta.SenderAddress,
},
mail.Address{Address: newEmail},
("Confirm your " + app.Settings().Meta.AppName + " new email address"),
body,
nil,
)
})
if sendErr == nil {
app.OnMailerAfterUserChangeEmailSend().Trigger(event)
}
return sendErr
}
+87
View File
@@ -0,0 +1,87 @@
package mails_test
import (
"strings"
"testing"
"github.com/pocketbase/pocketbase/mails"
"github.com/pocketbase/pocketbase/tests"
)
func TestSendUserPasswordReset(t *testing.T) {
testApp, _ := tests.NewTestApp()
defer testApp.Cleanup()
// ensure that action url normalization will be applied
testApp.Settings().Meta.AppUrl = "http://localhost:8090////"
user, _ := testApp.Dao().FindUserByEmail("test@example.com")
err := mails.SendUserPasswordReset(testApp, user)
if err != nil {
t.Fatal(err)
}
if testApp.TestMailer.TotalSend != 1 {
t.Fatalf("Expected one email to be sent, got %d", testApp.TestMailer.TotalSend)
}
expectedParts := []string{
"http://localhost:8090/#/users/confirm-password-reset/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.",
}
for _, part := range expectedParts {
if !strings.Contains(testApp.TestMailer.LastHtmlBody, part) {
t.Fatalf("Couldn't find %s \nin\n %s", part, testApp.TestMailer.LastHtmlBody)
}
}
}
func TestSendUserVerification(t *testing.T) {
testApp, _ := tests.NewTestApp()
defer testApp.Cleanup()
user, _ := testApp.Dao().FindUserByEmail("test@example.com")
err := mails.SendUserVerification(testApp, user)
if err != nil {
t.Fatal(err)
}
if testApp.TestMailer.TotalSend != 1 {
t.Fatalf("Expected one email to be sent, got %d", testApp.TestMailer.TotalSend)
}
expectedParts := []string{
"http://localhost:8090/#/users/confirm-verification/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.",
}
for _, part := range expectedParts {
if !strings.Contains(testApp.TestMailer.LastHtmlBody, part) {
t.Fatalf("Couldn't find %s \nin\n %s", part, testApp.TestMailer.LastHtmlBody)
}
}
}
func TestSendUserChangeEmail(t *testing.T) {
testApp, _ := tests.NewTestApp()
defer testApp.Cleanup()
user, _ := testApp.Dao().FindUserByEmail("test@example.com")
err := mails.SendUserChangeEmail(testApp, user, "new_test@example.com")
if err != nil {
t.Fatal(err)
}
if testApp.TestMailer.TotalSend != 1 {
t.Fatalf("Expected one email to be sent, got %d", testApp.TestMailer.TotalSend)
}
expectedParts := []string{
"http://localhost:8090/#/users/confirm-email-change/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.",
}
for _, part := range expectedParts {
if !strings.Contains(testApp.TestMailer.LastHtmlBody, part) {
t.Fatalf("Couldn't find %s \nin\n %s", part, testApp.TestMailer.LastHtmlBody)
}
}
}