Compare commits

...

19 Commits

Author SHA1 Message Date
Lakhan Samani
c7494a0bca fix: syntax 2021-10-10 01:50:36 +05:30
Lakhan Samani
e23d6f92e5 fix: syntax 2021-10-10 01:48:05 +05:30
Lakhan Samani
585091fefb fix: syntax 2021-10-10 01:47:25 +05:30
Lakhan Samani
eabd88718d chore: update github action version 2021-10-10 01:46:17 +05:30
Lakhan Samani
072cd46809 fix: docker build version arg 2021-10-10 01:15:47 +05:30
Lakhan Samani
17676fa13b fix: docker build version arg 2021-10-10 00:28:44 +05:30
Lakhan Samani
8b510ed556 fix: env parsing 2021-10-09 23:49:20 +05:30
Lakhan Samani
3ac0b44446 fix: remove unused env and fix typo 2021-10-09 22:29:10 +05:30
Lakhan Samani
b1dd6f2c3b chore: update app dependencies 2021-10-09 18:27:38 +05:30
Lakhan Samani
f5ea94f63c Merge branch 'main' of https://github.com/authorizerdev/authorizer 2021-10-09 10:06:15 +05:30
Lakhan Samani
653befc737 chore: update app dependencies (#56)
* fix: role validation for given token

* chore: update app dependencies
2021-10-09 10:04:59 +05:30
Lakhan Samani
6fed439ec2 Merge branch 'main' of https://github.com/authorizerdev/authorizer 2021-10-09 10:01:47 +05:30
Lakhan Samani
5bd6fa5bc9 fix: role validation for given token (#55) 2021-10-09 09:40:45 +05:30
Lakhan Samani
75709e9f48 fix: role validation for given token 2021-10-09 09:39:50 +05:30
Lakhan Samani
b1b7f47f4c fix: roles model 2021-10-04 13:58:03 +05:30
Lakhan Samani
e429a1f860 fix: github workflow build script 2021-10-04 13:12:51 +05:30
Lakhan Samani
6819597a79 fix: mac build script 2021-10-04 13:11:26 +05:30
Lakhan Samani
b34b385be5 fix: email template 2021-10-04 12:19:27 +05:30
Lakhan Samani
5acf59d16e feat: add responsive email template + support for org env
Resolves #52
2021-10-04 03:17:50 +05:30
18 changed files with 326 additions and 148 deletions

View File

@@ -41,18 +41,18 @@ jobs:
make clean && \
CGO_ENABLED=1 GOOS=windows CC=/usr/bin/x86_64-w64-mingw32-gcc make && \
mv build/server build/server.exe && \
zip -vr authorizer-${VERSION}-windows-amd64.zip .env app build templates
zip -vr authorizer-${VERSION}-windows-amd64.zip .env app/build build templates
- name: Package files for linux
run: |
make clean && \
CGO_ENABLED=1 make && \
tar cvfz authorizer-${VERSION}-linux-amd64.tar.gz .env app build templates
tar cvfz authorizer-${VERSION}-linux-amd64.tar.gz .env app/build build templates
- name: Upload assets
run: |
github-assets-uploader -f authorizer-${VERSION}-windows-amd64.zip -mediatype application/zip -repo authorizerdev/authorizer -token ${{secrets.RELEASE_TOKEN}} -tag ${VERSION} && \
github-assets-uploader -f authorizer-${VERSION}-linux-amd64.tar.gz -mediatype application/gzip -repo authorizerdev/authorizer -token ${{secrets.RELEASE_TOKEN}} -tag ${VERSION}
- name: Log in to Docker Hub
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
@@ -64,9 +64,11 @@ jobs:
images: lakhansamani/authorizer
- name: Build and push Docker image
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
uses: docker/build-push-action@v2
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
"VERSION=${{ VERSION }}"

View File

@@ -3,9 +3,10 @@ WORKDIR /app
COPY server server
COPY Makefile .
ARG VERSION=0.1.0-beta.0
ENV VERSION="${VERSION}"
ARG VERSION="latest"
ENV VERSION="$VERSION"
RUN echo "$VERSION"
RUN apk add build-base &&\
make clean && make && \
chmod 777 build/server

View File

@@ -14,3 +14,9 @@ For the first version we will only support setting roles master list via env
- [x] Return roles in users list for super admin
- [x] Add roles to the JWT token generation
- [x] Validate token should also validate the role, if roles to validate again is present in request
# Misc
- [x] Fix email template
- [x] Add support for organization name in .env
- [x] Add support for organization logo in .env

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

30
app/package-lock.json generated
View File

@@ -8,7 +8,7 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@authorizerdev/authorizer-react": "^0.1.0-beta.16",
"@authorizerdev/authorizer-react": "^0.1.0-beta.18",
"@types/react": "^17.0.15",
"@types/react-dom": "^17.0.9",
"esbuild": "^0.12.17",
@@ -22,9 +22,9 @@
}
},
"node_modules/@authorizerdev/authorizer-js": {
"version": "0.1.0-beta.16",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.1.0-beta.16.tgz",
"integrity": "sha512-RyjWhVbLYvmkcAT+2ddpRhrt7P5DBVKtGKjHWRugSqenTW7XFQMlhQqx5Z09DeqLw3Lsq0VVZ5h8tWcExHvwEw==",
"version": "0.1.0-beta.19",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.1.0-beta.19.tgz",
"integrity": "sha512-//uYjklwQfQKqLJHMAyjdrzh2nz6DycB3lEgl6bTXxmSbrz+l1kQyxB3y8wP/W30IrBQz8bZb+1sau+LD/FU7g==",
"dependencies": {
"node-fetch": "^2.6.1"
},
@@ -33,11 +33,11 @@
}
},
"node_modules/@authorizerdev/authorizer-react": {
"version": "0.1.0-beta.16",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.1.0-beta.16.tgz",
"integrity": "sha512-b00LH0gtfMh/opFaGDF+EkxDOpNkCj/TNVjyW2wbiOFdC4HgLhc+UbPgL5/rDDol4XQsToL3SmwxmSxB2lWWuQ==",
"version": "0.1.0-beta.18",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.1.0-beta.18.tgz",
"integrity": "sha512-lRWWlS9akZwwINRW1NatsbMob06NXht3HXNTUTlu1s8m1YjxmFRE/AL6UIplzAYTpR6eDWMxEEaS0qAVxovUcg==",
"dependencies": {
"@authorizerdev/authorizer-js": "^0.1.0-beta.16",
"@authorizerdev/authorizer-js": "^0.1.0-beta.19",
"final-form": "^4.20.2",
"react-final-form": "^6.5.3",
"styled-components": "^5.3.0"
@@ -797,19 +797,19 @@
},
"dependencies": {
"@authorizerdev/authorizer-js": {
"version": "0.1.0-beta.16",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.1.0-beta.16.tgz",
"integrity": "sha512-RyjWhVbLYvmkcAT+2ddpRhrt7P5DBVKtGKjHWRugSqenTW7XFQMlhQqx5Z09DeqLw3Lsq0VVZ5h8tWcExHvwEw==",
"version": "0.1.0-beta.19",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.1.0-beta.19.tgz",
"integrity": "sha512-//uYjklwQfQKqLJHMAyjdrzh2nz6DycB3lEgl6bTXxmSbrz+l1kQyxB3y8wP/W30IrBQz8bZb+1sau+LD/FU7g==",
"requires": {
"node-fetch": "^2.6.1"
}
},
"@authorizerdev/authorizer-react": {
"version": "0.1.0-beta.16",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.1.0-beta.16.tgz",
"integrity": "sha512-b00LH0gtfMh/opFaGDF+EkxDOpNkCj/TNVjyW2wbiOFdC4HgLhc+UbPgL5/rDDol4XQsToL3SmwxmSxB2lWWuQ==",
"version": "0.1.0-beta.18",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.1.0-beta.18.tgz",
"integrity": "sha512-lRWWlS9akZwwINRW1NatsbMob06NXht3HXNTUTlu1s8m1YjxmFRE/AL6UIplzAYTpR6eDWMxEEaS0qAVxovUcg==",
"requires": {
"@authorizerdev/authorizer-js": "^0.1.0-beta.16",
"@authorizerdev/authorizer-js": "^0.1.0-beta.19",
"final-form": "^4.20.2",
"react-final-form": "^6.5.3",
"styled-components": "^5.3.0"

View File

@@ -10,7 +10,7 @@
"author": "Lakhan Samani",
"license": "ISC",
"dependencies": {
"@authorizerdev/authorizer-react": "^0.1.0-beta.16",
"@authorizerdev/authorizer-react": "^0.1.0-beta.18",
"@types/react": "^17.0.15",
"@types/react-dom": "^17.0.9",
"esbuild": "^0.12.17",

View File

@@ -4,30 +4,52 @@ import { AuthorizerProvider } from '@authorizerdev/authorizer-react';
import Root from './Root';
export default function App() {
// @ts-ignore
const globalState: Record<string, string> = window['__authorizer__'];
return (
<div style={{ display: 'flex', justifyContent: 'center' }}>
<div
style={{
width: 400,
margin: `10px auto`,
border: `1px solid #D1D5DB`,
padding: `25px 20px`,
borderRadius: 5,
}}
>
<BrowserRouter>
<AuthorizerProvider
config={{
authorizerURL: globalState.authorizerURL,
redirectURL: globalState.redirectURL,
}}
>
<Root />
</AuthorizerProvider>
</BrowserRouter>
</div>
</div>
);
// @ts-ignore
const globalState: Record<string, string> = window['__authorizer__'];
return (
<div
style={{
display: 'flex',
justifyContent: 'center',
flexDirection: 'column',
}}
>
<div
style={{
display: 'flex',
justifyContent: 'center',
marginTop: 20,
flexDirection: 'column',
alignItems: 'center',
}}
>
<img
src={`${globalState.organizationLogo}`}
alt="logo"
style={{ height: 60, width: 60, objectFit: 'cover' }}
/>
<h1>{globalState.organizationName}</h1>
</div>
<div
style={{
width: 400,
margin: `10px auto`,
border: `1px solid #D1D5DB`,
padding: `25px 20px`,
borderRadius: 5,
}}
>
<BrowserRouter>
<AuthorizerProvider
config={{
authorizerURL: globalState.authorizerURL,
redirectURL: globalState.redirectURL,
}}
>
<Root />
</AuthorizerProvider>
</BrowserRouter>
</div>
</div>
);
}

View File

@@ -10,9 +10,9 @@ export default function Root() {
useEffect(() => {
if (token) {
const url = new URL(config.redirectURL);
const url = new URL(config.redirectURL || '/app');
if (url.origin !== window.location.origin) {
window.location.href = config.redirectURL;
window.location.href = config.redirectURL || '/app';
}
}
return () => {};

View File

@@ -2,9 +2,9 @@ import React, { Fragment } from 'react';
import { Authorizer } from '@authorizerdev/authorizer-react';
export default function Login() {
return (
<Fragment>
<Authorizer />
</Fragment>
);
return (
<Fragment>
<Authorizer />
</Fragment>
);
}

View File

@@ -1,7 +1,7 @@
VERSION="$1"
make clean && CGO_ENABLED=1 VERSION=${VERSION} make
FILE_NAME=authorizer-${VERSION}-darwin-amd64.tar.gz
tar cvfz ${FILE_NAME} .env app build templates
tar cvfz ${FILE_NAME} .env app/build build templates
AUTH="Authorization: token $GITHUB_TOKEN"
RELASE_INFO=$(curl -sH "$AUTH" https://api.github.com/repos/authorizerdev/authorizer/releases/tags/${VERSION})
echo $RELASE_INFO

View File

@@ -13,7 +13,6 @@ var (
JWT_TYPE = ""
JWT_SECRET = ""
ALLOWED_ORIGINS = []string{}
ALLOWED_CALLBACK_URLS = []string{}
AUTHORIZER_URL = ""
PORT = "8080"
REDIS_URL = ""
@@ -37,4 +36,8 @@ var (
FACEBOOK_CLIENT_SECRET = ""
TWITTER_CLIENT_ID = ""
TWITTER_CLIENT_SECRET = ""
// Org envs
ORGANIZATION_NAME = "Authorizer"
ORGANIZATION_LOGO = "https://authorizer.dev/images/logo.png"
)

View File

@@ -23,8 +23,7 @@ func (r *Role) BeforeCreate(tx *gorm.DB) (err error) {
func (mgr *manager) SaveRoles(roles []Role) error {
res := mgr.db.Clauses(
clause.OnConflict{
OnConstraint: "authorizer_roles_role_key",
DoNothing: true,
DoNothing: true,
}).Create(&roles)
if res.Error != nil {
log.Println(`Error saving roles`)

View File

@@ -11,43 +11,33 @@ import (
)
// build variables
var Version string
// ParseArgs -> to parse the cli flag and get db url. This is useful with heroku button
func ParseArgs() {
dbURL := flag.String("database_url", "", "Database connection string")
dbType := flag.String("databse_type", "", "Database type, possible values are postgres,mysql,sqlite")
authorizerURL := flag.String("authorizer_url", "", "URL for authorizer instance, eg: https://xyz.herokuapp.com")
flag.Parse()
if *dbURL != "" {
constants.DATABASE_URL = *dbURL
}
if *dbType != "" {
constants.DATABASE_TYPE = *dbType
}
if *authorizerURL != "" {
constants.AUTHORIZER_URL = *authorizerURL
}
}
var (
Version string
ARG_DB_URL *string
ARG_DB_TYPE *string
ARG_AUTHORIZER_URL *string
ARG_ENV_FILE *string
)
// InitEnv -> to initialize env and through error if required env are not present
func InitEnv() {
envPath := `.env`
envFile := flag.String("env_file", "", "Env file path")
ARG_DB_URL = flag.String("database_url", "", "Database connection string")
ARG_DB_TYPE = flag.String("database_type", "", "Database type, possible values are postgres,mysql,sqlite")
ARG_AUTHORIZER_URL = flag.String("authorizer_url", "", "URL for authorizer instance, eg: https://xyz.herokuapp.com")
ARG_ENV_FILE = flag.String("env_file", "", "Env file path")
flag.Parse()
if *envFile != "" {
envPath = *envFile
if *ARG_ENV_FILE != "" {
envPath = *ARG_ENV_FILE
}
err := godotenv.Load(envPath)
if err != nil {
log.Println("Error loading .env file")
}
constants.VERSION = Version
constants.ADMIN_SECRET = os.Getenv("ADMIN_SECRET")
constants.ENV = os.Getenv("ENV")
constants.DATABASE_TYPE = os.Getenv("DATABASE_TYPE")
@@ -104,20 +94,18 @@ func InitEnv() {
}
constants.ALLOWED_ORIGINS = allowedOrigins
allowedCallbackSplit := strings.Split(os.Getenv("ALLOWED_CALLBACK_URLS"), ",")
allowedCallbacks := []string{}
for _, val := range allowedCallbackSplit {
trimVal := strings.TrimSpace(val)
if trimVal != "" {
allowedCallbacks = append(allowedCallbacks, trimVal)
}
if *ARG_AUTHORIZER_URL != "" {
constants.AUTHORIZER_URL = *ARG_AUTHORIZER_URL
}
if *ARG_DB_URL != "" {
constants.DATABASE_URL = *ARG_DB_URL
}
if *ARG_DB_TYPE != "" {
constants.DATABASE_TYPE = *ARG_DB_TYPE
}
if len(allowedCallbackSplit) == 0 {
allowedCallbackSplit = []string{"*"}
}
constants.ALLOWED_CALLBACK_URLS = allowedCallbackSplit
ParseArgs()
if constants.DATABASE_URL == "" {
panic("Database url is required")
}
@@ -174,4 +162,12 @@ func InitEnv() {
if constants.JWT_ROLE_CLAIM == "" {
constants.JWT_ROLE_CLAIM = "role"
}
if os.Getenv("ORGANIZATION_NAME") != "" {
constants.ORGANIZATION_NAME = os.Getenv("ORGANIZATION_NAME")
}
if os.Getenv("ORGANIZATION_LOGO") != "" {
constants.ORGANIZATION_LOGO = os.Getenv("ORGANIZATION_LOGO")
}
}

View File

@@ -75,8 +75,10 @@ func AppHandler() gin.HandlerFunc {
}
c.HTML(http.StatusOK, "app.tmpl", gin.H{
"data": map[string]string{
"authorizerURL": stateObj.AuthorizerURL,
"redirectURL": stateObj.RedirectURL,
"authorizerURL": stateObj.AuthorizerURL,
"redirectURL": stateObj.RedirectURL,
"organizationName": constants.ORGANIZATION_NAME,
"organizationLogo": constants.ORGANIZATION_LOGO,
},
})
}

View File

@@ -30,7 +30,6 @@ func AdminUpdateUser(ctx context.Context, params model.AdminUpdateUserInput) (*m
}
user, err := db.Mgr.GetUserByID(params.ID)
if err != nil {
return res, fmt.Errorf(`User not found`)
}
@@ -114,9 +113,8 @@ func AdminUpdateUser(ctx context.Context, params model.AdminUpdateUserInput) (*m
return res, err
}
userIdStr := fmt.Sprintf("%v", user.ID)
res = &model.User{
ID: userIdStr,
ID: params.ID,
Email: user.Email,
Image: &user.Image,
FirstName: &user.FirstName,

View File

@@ -36,8 +36,8 @@ func Token(ctx context.Context, role *string) (*model.AuthResponse, error) {
return res, err
}
if role != nil && role != &claimRole {
return res, fmt.Errorf(`unauthorized. invalid role for a given token`)
if role != nil && *role != claimRole {
return res, fmt.Errorf(`unauthorized`)
}
userIdStr := fmt.Sprintf("%v", user.ID)

View File

@@ -16,17 +16,91 @@ func SendVerificationMail(toEmail, token string) error {
Subject := "Please verify your email"
message := fmt.Sprintf(`
<!DOCTYPE HTML PULBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="content-type" content="text/html"; charset=ISO-8859-1">
</head>
<body>
<h1>Please verify your email by clicking on the link below </h1><br/>
<a href="%s">Click here to verify</a>
</body>
</html>
`, constants.AUTHORIZER_URL+"/verify_email"+"?token="+token)
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1" name="viewport">
<meta name="x-apple-disable-message-reformatting">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta content="telephone=no" name="format-detection">
<title></title>
<!--[if (mso 16)]>
<style type="text/css">
a {}
</style>
<![endif]-->
<!--[if gte mso 9]><style>sup { font-size: 100%% !important; }</style><![endif]-->
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG></o:AllowPNG>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
</head>
<body style="font-family: sans-serif;">
<div class="es-wrapper-color">
<!--[if gte mso 9]>
<v:background xmlns:v="urn:schemas-microsoft-com:vml" fill="t">
<v:fill type="tile" color="#ffffff"></v:fill>
</v:background>
<![endif]-->
<table class="es-wrapper" width="100%%" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td class="esd-email-paddings" valign="top">
<table class="es-content esd-footer-popover" cellspacing="0" cellpadding="0" align="center">
<tbody>
<tr>
<td class="esd-stripe" align="center">
<table class="es-content-body" style="border-left:1px solid transparent;border-right:1px solid transparent;border-top:1px solid transparent;border-bottom:1px solid transparent;padding:20px 0px;" width="600" cellspacing="0" cellpadding="0" bgcolor="#ffffff" align="center">
<tbody>
<tr>
<td class="esd-structure es-p20t es-p40b es-p40r es-p40l" esd-custom-block-id="8537" align="left">
<table width="100%%" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td class="esd-container-frame" width="518" align="left">
<table width="100%%" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td class="esd-block-image es-m-txt-c es-p5b" style="font-size:0;padding:10px" align="center"><a target="_blank"><img src="%s" alt="icon" style="display: block;" title="icon" width="30"></a></td>
</tr>
<tr style="background: rgb(249,250,251);padding: 10px;margin-bottom:10px;border-radius:5px;">
<td class="esd-block-text es-m-txt-c es-p15t" align="center" style="padding:10px;padding-bottom:30px;">
<p>Hey there 👋</p>
<p>We received a request to sign-up for <b>%s</b>. If this is correct, please confirm your email address by clicking the button below.</p> <br/>
<a href="%s" class="es-button" target="_blank" style="text-decoration: none;padding:10px 15px;background-color: rgba(59,130,246,1);color: #fff;font-size: 1em;border-radius:5px;">Confirm Email</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<div style="position: absolute; left: -9999px; top: -9999px; margin: 0px;"></div>
</body>
</html>
`, constants.ORGANIZATION_LOGO, constants.ORGANIZATION_NAME, constants.AUTHORIZER_URL+"/verify_email"+"?token="+token)
bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message)
return sender.SendMail(Receiver, Subject, bodyMessage)
@@ -46,17 +120,92 @@ func SendForgotPasswordMail(toEmail, token, host string) error {
Subject := "Reset Password"
message := fmt.Sprintf(`
<!DOCTYPE HTML PULBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="content-type" content="text/html"; charset=ISO-8859-1">
</head>
<body>
<h1>Please use the link below to reset password </h1><br/>
<a href="%s">Reset Password</a>
</body>
</html>
`, constants.RESET_PASSWORD_URL+"?token="+token)
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1" name="viewport">
<meta name="x-apple-disable-message-reformatting">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta content="telephone=no" name="format-detection">
<title></title>
<!--[if (mso 16)]>
<style type="text/css">
a {}
</style>
<![endif]-->
<!--[if gte mso 9]><style>sup { font-size: 100%% !important; }</style><![endif]-->
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG></o:AllowPNG>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
</head>
<body style="font-family: sans-serif;">
<div class="es-wrapper-color">
<!--[if gte mso 9]>
<v:background xmlns:v="urn:schemas-microsoft-com:vml" fill="t">
<v:fill type="tile" color="#ffffff"></v:fill>
</v:background>
<![endif]-->
<table class="es-wrapper" width="100%%" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td class="esd-email-paddings" valign="top">
<table class="es-content esd-footer-popover" cellspacing="0" cellpadding="0" align="center">
<tbody>
<tr>
<td class="esd-stripe" align="center">
<table class="es-content-body" style="border-left:1px solid transparent;border-right:1px solid transparent;border-top:1px solid transparent;border-bottom:1px solid transparent;padding:20px 0px;" width="600" cellspacing="0" cellpadding="0" bgcolor="#ffffff" align="center">
<tbody>
<tr>
<td class="esd-structure es-p20t es-p40b es-p40r es-p40l" esd-custom-block-id="8537" align="left">
<table width="100%%" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td class="esd-container-frame" width="518" align="left">
<table width="100%%" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td class="esd-block-image es-m-txt-c es-p5b" style="font-size:0;padding:10px" align="center"><a target="_blank"><img src="%s" alt="icon" style="display: block;" title="icon" width="30"></a></td>
</tr>
<tr style="background: rgb(249,250,251);padding: 10px;margin-bottom:10px;border-radius:5px;">
<td class="esd-block-text es-m-txt-c es-p15t" align="center" style="padding:10px;padding-bottom:30px;">
<p>Hey there 👋</p>
<p>We received a request to reset password for email: <b>%s</b>. If this is correct, please reset the password clicking the button below.</p> <br/>
<a href="%s" class="es-button" target="_blank" style="text-decoration: none;padding:10px 15px;background-color: rgba(59,130,246,1);color: #fff;font-size: 1em;border-radius:5px;">Reset Password</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<div style="position: absolute; left: -9999px; top: -9999px; margin: 0px;"></div>
</body>
</html>
`, constants.ORGANIZATION_LOGO, toEmail, constants.RESET_PASSWORD_URL+"?token="+token)
bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message)
return sender.SendMail(Receiver, Subject, bodyMessage)