Merge pull request #105 from grant-project/change-email
Settings redesign + change email + change password conf email
This commit is contained in:
commit
236d5b1ccf
|
@ -1,6 +1,6 @@
|
||||||
# Grant.io Admin UI
|
# ZF Grants Admin UI
|
||||||
|
|
||||||
This is the admin component of [Grant.io](http://grant.io).
|
This is the admin component of [grants.zfnd.org](http://grants.zfnd.org).
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ export default class Example extends React.Component<Props> {
|
||||||
<div className="Example-inbox-left">
|
<div className="Example-inbox-left">
|
||||||
<div className="Example-inbox-left-icon is-checkbox" />
|
<div className="Example-inbox-left-icon is-checkbox" />
|
||||||
<div className="Example-inbox-left-icon is-favorite" />
|
<div className="Example-inbox-left-icon is-favorite" />
|
||||||
<div className="Example-inbox-left-sender">Grant.io</div>
|
<div className="Example-inbox-left-sender">ZF Grants</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="Example-inbox-subject">
|
<div className="Example-inbox-subject">
|
||||||
<strong>{email.info.subject}</strong>
|
<strong>{email.info.subject}</strong>
|
||||||
|
|
|
@ -16,6 +16,21 @@ export default [
|
||||||
title: 'Password recovery',
|
title: 'Password recovery',
|
||||||
description: 'For recovering a user’s forgotten password',
|
description: 'For recovering a user’s forgotten password',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'change_email',
|
||||||
|
title: 'Change email confirmation',
|
||||||
|
description: 'Sent when the user has changed their email, to confirm their new one',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'change_email_old',
|
||||||
|
title: 'Change email notification (Old email)',
|
||||||
|
description: 'Sent when the user has changed their email, in case of compromise',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'change_password',
|
||||||
|
title: 'Change password confirmation',
|
||||||
|
description: 'Sent when the user has changed their password, in case of compromise',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'team_invite',
|
id: 'team_invite',
|
||||||
title: 'Proposal team invite',
|
title: 'Proposal team invite',
|
||||||
|
|
|
@ -30,7 +30,7 @@ class Template extends React.Component<Props> {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Sider className="Template-sider">
|
<Sider className="Template-sider">
|
||||||
<div className="Template-sider-logo">grant.io</div>
|
<div className="Template-sider-logo">ZF Grants</div>
|
||||||
<Menu theme="dark" mode="inline" selectedKeys={[pathname]}>
|
<Menu theme="dark" mode="inline" selectedKeys={[pathname]}>
|
||||||
<Menu.Item key="/">
|
<Menu.Item key="/">
|
||||||
<Link to="/">
|
<Link to="/">
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charSet="utf-8" />
|
<meta charSet="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Admin - Grant.io</title>
|
<title>Admin - ZF Grants</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# Environment variable overrides for local development
|
# Environment variable overrides for local development
|
||||||
FLASK_APP=app.py
|
FLASK_APP=app.py
|
||||||
FLASK_ENV=development
|
FLASK_ENV=development
|
||||||
SITE_URL="https://grant.io" # No trailing slash
|
SITE_URL="https://zfnd.org" # No trailing slash
|
||||||
DATABASE_URL="sqlite:////tmp/dev.db"
|
DATABASE_URL="sqlite:////tmp/dev.db"
|
||||||
REDISTOGO_URL="redis://localhost:6379"
|
REDISTOGO_URL="redis://localhost:6379"
|
||||||
SECRET_KEY="not-so-secret"
|
SECRET_KEY="not-so-secret"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Grant.io Backend
|
# ZF Grants Backend
|
||||||
|
|
||||||
This is the backend component of [Grant.io](http://grant.io).
|
This is the backend component of [grants.zfnd.org](http://grants.zfnd.org).
|
||||||
|
|
||||||
## Environment Setup
|
## Environment Setup
|
||||||
|
|
||||||
|
@ -89,16 +89,6 @@ To create a proposal, run
|
||||||
|
|
||||||
flask create_proposal "FUNDING_REQUIRED" 1 123 "My Awesome Proposal" "### Hi! I have a great proposal"
|
flask create_proposal "FUNDING_REQUIRED" 1 123 "My Awesome Proposal" "### Hi! I have a great proposal"
|
||||||
|
|
||||||
## External Services
|
|
||||||
|
|
||||||
To decode EIP-712 signed messages, a Grant.io deployed service was created `https://eip-712.herokuapp.com`.
|
|
||||||
|
|
||||||
To adjust this endpoint, simply export `AUTH_URL` with a new endpoint value:
|
|
||||||
|
|
||||||
export AUTH_URL=http://new-endpoint.com
|
|
||||||
|
|
||||||
To learn more about this auth service, you can visit the repo [here](https://github.com/grant-project/eip-712-server).
|
|
||||||
|
|
||||||
## S3 Storage Setup
|
## S3 Storage Setup
|
||||||
|
|
||||||
1. create bucket, keep the `bucket name` and `region` handy
|
1. create bucket, keep the `bucket name` and `region` handy
|
||||||
|
@ -158,7 +148,7 @@ These instructions are for `development`, for `production` simply replace all ho
|
||||||
1. Create Twitter oauth app https://developer.twitter.com/en/apply/user
|
1. Create Twitter oauth app https://developer.twitter.com/en/apply/user
|
||||||
|
|
||||||
1. click **Create an App**
|
1. click **Create an App**
|
||||||
1. set **Website URL** to a valid URL, such as `http://demo.grant.io`
|
1. set **Website URL** to a valid URL, such as `http://grants.zfnd.org`
|
||||||
1. check the **Enable Sign in with Twitter** option
|
1. check the **Enable Sign in with Twitter** option
|
||||||
1. set **Callback URLs** to `http://127.0.0.1:3000/callback/twitter`
|
1. set **Callback URLs** to `http://127.0.0.1:3000/callback/twitter`
|
||||||
1. fill out other required fields
|
1. fill out other required fields
|
||||||
|
|
|
@ -35,6 +35,7 @@ update = FakeUpdate()
|
||||||
|
|
||||||
example_email_args = {
|
example_email_args = {
|
||||||
'signup': {
|
'signup': {
|
||||||
|
'display_name': user.display_name,
|
||||||
'confirm_url': 'http://someconfirmurl.com',
|
'confirm_url': 'http://someconfirmurl.com',
|
||||||
},
|
},
|
||||||
'team_invite': {
|
'team_invite': {
|
||||||
|
@ -45,6 +46,19 @@ example_email_args = {
|
||||||
'recover': {
|
'recover': {
|
||||||
'recover_url': 'http://somerecoveryurl.com',
|
'recover_url': 'http://somerecoveryurl.com',
|
||||||
},
|
},
|
||||||
|
'change_email': {
|
||||||
|
'display_name': user.display_name,
|
||||||
|
'confirm_url': 'http://someconfirmurl.com',
|
||||||
|
},
|
||||||
|
'change_email_old': {
|
||||||
|
'display_name': user.display_name,
|
||||||
|
'contact_url': 'http://somecontacturl.com',
|
||||||
|
},
|
||||||
|
'change_password': {
|
||||||
|
'display_name': user.display_name,
|
||||||
|
'recover_url': 'http://somerecoverurl.com',
|
||||||
|
'contact_url': 'http://somecontacturl.com',
|
||||||
|
},
|
||||||
'proposal_approved': {
|
'proposal_approved': {
|
||||||
'proposal': proposal,
|
'proposal': proposal,
|
||||||
'proposal_url': 'http://someproposal.com',
|
'proposal_url': 'http://someproposal.com',
|
||||||
|
|
|
@ -1,24 +1,25 @@
|
||||||
import sendgrid
|
import sendgrid
|
||||||
from flask import render_template, Markup, current_app
|
from flask import render_template, Markup, current_app
|
||||||
from grant.settings import SENDGRID_API_KEY, SENDGRID_DEFAULT_FROM
|
from grant.settings import SENDGRID_API_KEY, SENDGRID_DEFAULT_FROM, UI
|
||||||
|
from grant.utils.misc import make_url
|
||||||
from python_http_client import HTTPError
|
from python_http_client import HTTPError
|
||||||
from sendgrid.helpers.mail import Email, Mail, Content
|
from sendgrid.helpers.mail import Email, Mail, Content
|
||||||
|
|
||||||
from .subscription_settings import EmailSubscription, is_subscribed
|
from .subscription_settings import EmailSubscription, is_subscribed
|
||||||
|
|
||||||
default_template_args = {
|
default_template_args = {
|
||||||
'home_url': 'https://grant.io',
|
'home_url': make_url('/'),
|
||||||
'account_url': 'https://grant.io/user',
|
'account_url': make_url('/user'),
|
||||||
'email_settings_url': 'https://grant.io/user/settings',
|
'email_settings_url': make_url('/settings'),
|
||||||
'unsubscribe_url': 'https://grant.io/unsubscribe',
|
'unsubscribe_url': make_url('/unsubscribe'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def signup_info(email_args):
|
def signup_info(email_args):
|
||||||
return {
|
return {
|
||||||
'subject': 'Confirm your email on Grant.io',
|
'subject': 'Confirm your email on {}'.format(UI['NAME']),
|
||||||
'title': 'Welcome to Grant.io!',
|
'title': 'Welcome to {}!'.format(UI['NAME']),
|
||||||
'preview': 'Welcome to Grant.io, we just need to confirm your email address.',
|
'preview': 'Welcome to {}, we just need to confirm your email address.'.format(UI['NAME']),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,12 +33,36 @@ def team_invite_info(email_args):
|
||||||
|
|
||||||
def recover_info(email_args):
|
def recover_info(email_args):
|
||||||
return {
|
return {
|
||||||
'subject': 'Grant.io account recovery',
|
'subject': '{} account recovery'.format(UI['NAME']),
|
||||||
'title': 'Grant.io account recovery',
|
'title': '{} account recovery'.format(UI['NAME']),
|
||||||
'preview': 'Use the link to recover your account.'
|
'preview': 'Use the link to recover your account.'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def change_email_info(email_args):
|
||||||
|
return {
|
||||||
|
'subject': 'Confirm your new email',
|
||||||
|
'title': 'Confirm your email',
|
||||||
|
'preview': 'Click the link inside to confirm your new email'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def change_email_old_info(email_args):
|
||||||
|
return {
|
||||||
|
'subject': 'Your email has been changed',
|
||||||
|
'title': 'Email changed',
|
||||||
|
'preview': 'Your email address has been updated on {}'.format(UI['NAME']),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def change_password_info(email_args):
|
||||||
|
return {
|
||||||
|
'subject': 'Your password has been changed',
|
||||||
|
'title': 'Password changed',
|
||||||
|
'preview': 'This is just a confirmation of your recent password change'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def proposal_approved(email_args):
|
def proposal_approved(email_args):
|
||||||
return {
|
return {
|
||||||
'subject': 'Your proposal has been approved!',
|
'subject': 'Your proposal has been approved!',
|
||||||
|
@ -118,6 +143,9 @@ get_info_lookup = {
|
||||||
'signup': signup_info,
|
'signup': signup_info,
|
||||||
'team_invite': team_invite_info,
|
'team_invite': team_invite_info,
|
||||||
'recover': recover_info,
|
'recover': recover_info,
|
||||||
|
'change_email': change_email_info,
|
||||||
|
'change_email_old': change_email_old_info,
|
||||||
|
'change_password': change_password_info,
|
||||||
'proposal_approved': proposal_approved,
|
'proposal_approved': proposal_approved,
|
||||||
'proposal_rejected': proposal_rejected,
|
'proposal_rejected': proposal_rejected,
|
||||||
'proposal_contribution': proposal_contribution,
|
'proposal_contribution': proposal_contribution,
|
||||||
|
@ -130,19 +158,35 @@ get_info_lookup = {
|
||||||
|
|
||||||
def generate_email(type, email_args):
|
def generate_email(type, email_args):
|
||||||
info = get_info_lookup[type](email_args)
|
info = get_info_lookup[type](email_args)
|
||||||
body_text = render_template('emails/%s.txt' % (type), args=email_args)
|
body_text = render_template(
|
||||||
body_html = render_template('emails/%s.html' % (type), args=email_args)
|
'emails/%s.txt' % (type),
|
||||||
|
args=email_args,
|
||||||
|
UI=UI,
|
||||||
|
)
|
||||||
|
body_html = render_template(
|
||||||
|
'emails/%s.html' % (type),
|
||||||
|
args=email_args,
|
||||||
|
UI=UI,
|
||||||
|
)
|
||||||
|
|
||||||
html = render_template('emails/template.html', args={
|
html = render_template(
|
||||||
**default_template_args,
|
'emails/template.html',
|
||||||
**info,
|
args={
|
||||||
'body': Markup(body_html),
|
**default_template_args,
|
||||||
})
|
**info,
|
||||||
text = render_template('emails/template.txt', args={
|
'body': Markup(body_html),
|
||||||
**default_template_args,
|
},
|
||||||
**info,
|
UI=UI,
|
||||||
'body': body_text,
|
)
|
||||||
})
|
text = render_template(
|
||||||
|
'emails/template.txt',
|
||||||
|
args={
|
||||||
|
**default_template_args,
|
||||||
|
**info,
|
||||||
|
'body': body_text,
|
||||||
|
},
|
||||||
|
UI=UI,
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'info': info,
|
'info': info,
|
||||||
|
|
|
@ -13,7 +13,7 @@ env.read_env()
|
||||||
|
|
||||||
ENV = env.str("FLASK_ENV", default="production")
|
ENV = env.str("FLASK_ENV", default="production")
|
||||||
DEBUG = ENV == "development"
|
DEBUG = ENV == "development"
|
||||||
SITE_URL = env.str('SITE_URL', default='https://grant.io')
|
SITE_URL = env.str('SITE_URL', default='https://zfnd.org')
|
||||||
SQLALCHEMY_DATABASE_URI = env.str("DATABASE_URL")
|
SQLALCHEMY_DATABASE_URI = env.str("DATABASE_URL")
|
||||||
QUEUES = ["default"]
|
QUEUES = ["default"]
|
||||||
SECRET_KEY = env.str("SECRET_KEY")
|
SECRET_KEY = env.str("SECRET_KEY")
|
||||||
|
@ -24,7 +24,7 @@ CACHE_TYPE = "simple" # Can be "memcached", "redis", etc.
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
|
|
||||||
SENDGRID_API_KEY = env.str("SENDGRID_API_KEY", default="")
|
SENDGRID_API_KEY = env.str("SENDGRID_API_KEY", default="")
|
||||||
SENDGRID_DEFAULT_FROM = "noreply@grant.io"
|
SENDGRID_DEFAULT_FROM = "noreply@zfnd.org"
|
||||||
|
|
||||||
SENTRY_DSN = env.str("SENTRY_DSN", default=None)
|
SENTRY_DSN = env.str("SENTRY_DSN", default=None)
|
||||||
SENTRY_RELEASE = env.str("SENTRY_RELEASE", default=None)
|
SENTRY_RELEASE = env.str("SENTRY_RELEASE", default=None)
|
||||||
|
@ -51,3 +51,9 @@ BLOCKCHAIN_REST_API_URL = env.str("BLOCKCHAIN_REST_API_URL")
|
||||||
BLOCKCHAIN_API_SECRET = env.str("BLOCKCHAIN_API_SECRET")
|
BLOCKCHAIN_API_SECRET = env.str("BLOCKCHAIN_API_SECRET")
|
||||||
|
|
||||||
ADMIN_PASS_HASH = env.str("ADMIN_PASS_HASH")
|
ADMIN_PASS_HASH = env.str("ADMIN_PASS_HASH")
|
||||||
|
|
||||||
|
UI = {
|
||||||
|
'NAME': 'ZF Grants',
|
||||||
|
'PRIMARY': '#CF8A00',
|
||||||
|
'SECONDARY': '#2D2A26',
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
<p style="margin: 0;">
|
||||||
|
Hey {{ args.display_name }}, you just changed your email. In order to confirm this change, just click the button below.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||||
|
<tr>
|
||||||
|
<td bgcolor="#ffffff" align="center" style="padding: 40px 30px 40px 30px;">
|
||||||
|
<table border="0" cellspacing="0" cellpadding="0">
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="border-radius: 3px;" bgcolor="#CF8A00">
|
||||||
|
<a href="{{ args.confirm_url }}" target="_blank"
|
||||||
|
style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 20px 50px; border-radius: 4px; border: 1px solid #CF8A00; display: inline-block;">
|
||||||
|
Confirm Email
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="margin: 0 0 10px; font-size: 14px; text-align: center;">
|
||||||
|
If that doesn't work, copy and paste the following link in your browser
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="margin: 0 0 30px; font-size: 12px; text-align: center;">
|
||||||
|
<a href="{{ args.confirm_url }}" target="_blank" style="color: #CF8A00;">{{ args.confirm_url }}</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="margin: 0; font-size: 10px; color: #AAA; text-align: center;">
|
||||||
|
Don’t know why you got this email? Don’t worry, you can safely ignore it. We won’t send you anymore.
|
||||||
|
</p>
|
|
@ -0,0 +1,5 @@
|
||||||
|
Hey {{ args.display_name }}, you just changed your email. In order to confirm this change, you’ll need to visit the link below.
|
||||||
|
|
||||||
|
{{ args.confirm_url }}
|
||||||
|
|
||||||
|
Don’t know why you got this email? Don’t worry, you can safely ignore it. We won’t send you anymore.
|
|
@ -0,0 +1,14 @@
|
||||||
|
<p style="margin: 0 0 20px;">
|
||||||
|
Hey {{ args.display_name }}, you just changed your email. Your new email
|
||||||
|
address was also sent an email to confirm your new one. If you did this,
|
||||||
|
you can safely delete this message.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="margin: 0;">
|
||||||
|
If it wasn't you who did this, you should
|
||||||
|
<a href="{{ args.contact_url }}" target="_blank" style="color: #CF8A00;">
|
||||||
|
contact support
|
||||||
|
</a>
|
||||||
|
immediately.
|
||||||
|
</p>
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
Hey {{ args.display_name }}, you just changed your email. Your new email address was also sent an email to confirm your new one. If you did this, you can safely delete this message.
|
||||||
|
|
||||||
|
If it wasn't you who did this, you should contact support immediately.
|
||||||
|
|
||||||
|
{{ args.contact_url }}
|
|
@ -0,0 +1,16 @@
|
||||||
|
<p style="margin: 0 0 20px;">
|
||||||
|
Hey {{ args.display_name }}, you just changed your password. If you did this,
|
||||||
|
you can safely delete this email.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="margin: 0;">
|
||||||
|
If it wasn't you who did this, you should
|
||||||
|
<a href="{{ args.recover_url }}" target="_blank" style="color: #CF8A00;">
|
||||||
|
recover your account
|
||||||
|
</a>
|
||||||
|
or
|
||||||
|
<a href="{{ args.contact_url }}" target="_blank" style="color: #CF8A00;">
|
||||||
|
contact support
|
||||||
|
</a>
|
||||||
|
immediately.
|
||||||
|
</p>
|
|
@ -0,0 +1,6 @@
|
||||||
|
Hey {{ args.display_name }}, you just changed your password. If you did this, you can safely delete this email.
|
||||||
|
|
||||||
|
If it wasn't you who did this, you should recover your account or contact support immediately.
|
||||||
|
|
||||||
|
Recover account: {{ args.recover_url }}
|
||||||
|
Contact support: {{ args.contact_url }}
|
|
@ -13,9 +13,9 @@
|
||||||
<td bgcolor="#ffffff" align="center" style="padding: 40px 30px 40px 30px;">
|
<td bgcolor="#ffffff" align="center" style="padding: 40px 30px 40px 30px;">
|
||||||
<table border="0" cellspacing="0" cellpadding="0">
|
<table border="0" cellspacing="0" cellpadding="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="border-radius: 3px;" bgcolor="#530EEC">
|
<td align="center" style="border-radius: 3px;" bgcolor="{{ UI.PRIMARY }}">
|
||||||
<a href="{{ args.comment_url }}" target="_blank"
|
<a href="{{ args.comment_url }}" target="_blank"
|
||||||
style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 20px 50px; border-radius: 4px; border: 1px solid #530EEC; display: inline-block;">
|
style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 20px 50px; border-radius: 4px; border: 1px solid {{ UI.PRIMARY }}; display: inline-block;">
|
||||||
View their comment
|
View their comment
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -15,11 +15,11 @@
|
||||||
<td bgcolor="#ffffff" align="center" style="padding: 40px 30px 40px 30px;">
|
<td bgcolor="#ffffff" align="center" style="padding: 40px 30px 40px 30px;">
|
||||||
<table border="0" cellspacing="0" cellpadding="0">
|
<table border="0" cellspacing="0" cellpadding="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="border-radius: 3px;" bgcolor="#530EEC">
|
<td align="center" style="border-radius: 3px;" bgcolor="{{ UI.PRIMARY }}">
|
||||||
<a
|
<a
|
||||||
href="{{ args.update_url }}"
|
href="{{ args.update_url }}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 20px 50px; border-radius: 4px; border: 1px solid #530EEC; display: inline-block;"
|
style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 20px 50px; border-radius: 4px; border: 1px solid {{ UI.PRIMARY }}; display: inline-block;"
|
||||||
>
|
>
|
||||||
View the full update
|
View the full update
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -18,11 +18,11 @@
|
||||||
<td bgcolor="#ffffff" align="center" style="padding: 40px 30px 40px 30px;">
|
<td bgcolor="#ffffff" align="center" style="padding: 40px 30px 40px 30px;">
|
||||||
<table border="0" cellspacing="0" cellpadding="0">
|
<table border="0" cellspacing="0" cellpadding="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="border-radius: 3px;" bgcolor="#530EEC">
|
<td align="center" style="border-radius: 3px;" bgcolor="{{ UI.PRIMARY }}">
|
||||||
<a
|
<a
|
||||||
href="{{ args.proposal_url }}"
|
href="{{ args.proposal_url }}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 20px 50px; border-radius: 4px; border: 1px solid #530EEC; display: inline-block;"
|
style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 20px 50px; border-radius: 4px; border: 1px solid {{ UI.PRIMARY }}; display: inline-block;"
|
||||||
>
|
>
|
||||||
Publish your proposal
|
Publish your proposal
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -12,9 +12,9 @@
|
||||||
<td bgcolor="#ffffff" align="center" style="padding: 40px 30px 40px 30px;">
|
<td bgcolor="#ffffff" align="center" style="padding: 40px 30px 40px 30px;">
|
||||||
<table border="0" cellspacing="0" cellpadding="0">
|
<table border="0" cellspacing="0" cellpadding="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="border-radius: 3px;" bgcolor="#530EEC">
|
<td align="center" style="border-radius: 3px;" bgcolor="{{ UI.PRIMARY }}">
|
||||||
<a href="{{ args.comment_url }}" target="_blank"
|
<a href="{{ args.comment_url }}" target="_blank"
|
||||||
style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 20px 50px; border-radius: 4px; border: 1px solid #530EEC; display: inline-block;">
|
style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 20px 50px; border-radius: 4px; border: 1px solid {{ UI.PRIMARY }}; display: inline-block;">
|
||||||
View their comment
|
View their comment
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -11,9 +11,9 @@
|
||||||
<td bgcolor="#ffffff" align="center" style="padding: 40px 30px 40px 30px;">
|
<td bgcolor="#ffffff" align="center" style="padding: 40px 30px 40px 30px;">
|
||||||
<table border="0" cellspacing="0" cellpadding="0">
|
<table border="0" cellspacing="0" cellpadding="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="border-radius: 3px;" bgcolor="#530EEC">
|
<td align="center" style="border-radius: 3px;" bgcolor="{{ UI.PRIMARY }}">
|
||||||
<a href="{{ args.proposal_url }}" target="_blank"
|
<a href="{{ args.proposal_url }}" target="_blank"
|
||||||
style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 20px 50px; border-radius: 4px; border: 1px solid #530EEC; display: inline-block;">
|
style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 20px 50px; border-radius: 4px; border: 1px solid {{ UI.PRIMARY }}; display: inline-block;">
|
||||||
View your Proposal
|
View your Proposal
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -8,11 +8,11 @@
|
||||||
<td bgcolor="#ffffff" align="center" style="padding: 40px 30px 40px 30px;">
|
<td bgcolor="#ffffff" align="center" style="padding: 40px 30px 40px 30px;">
|
||||||
<table border="0" cellspacing="0" cellpadding="0">
|
<table border="0" cellspacing="0" cellpadding="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="border-radius: 3px;" bgcolor="#530EEC">
|
<td align="center" style="border-radius: 3px;" bgcolor="{{ UI.PRIMARY }}">
|
||||||
<a
|
<a
|
||||||
href="{{ args.recover_url }}"
|
href="{{ args.recover_url }}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 20px 50px; border-radius: 4px; border: 1px solid #530EEC; display: inline-block;"
|
style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 20px 50px; border-radius: 4px; border: 1px solid {{ UI.PRIMARY }}; display: inline-block;"
|
||||||
>
|
>
|
||||||
Reset Password
|
Reset Password
|
||||||
</a>
|
</a>
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="margin: 0 0 30px; font-size: 12px; text-align: center;">
|
<p style="margin: 0 0 30px; font-size: 12px; text-align: center;">
|
||||||
<a href="{{ args.recover_url }}" target="_blank" style="color: #530EEC;">{{
|
<a href="{{ args.recover_url }}" target="_blank" style="color: {{ UI.PRIMARY }};">{{
|
||||||
args.recover_url
|
args.recover_url
|
||||||
}}</a>
|
}}</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -7,9 +7,9 @@
|
||||||
<td bgcolor="#ffffff" align="center" style="padding: 40px 30px 40px 30px;">
|
<td bgcolor="#ffffff" align="center" style="padding: 40px 30px 40px 30px;">
|
||||||
<table border="0" cellspacing="0" cellpadding="0">
|
<table border="0" cellspacing="0" cellpadding="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="border-radius: 3px;" bgcolor="#530EEC">
|
<td align="center" style="border-radius: 3px;" bgcolor="{{ UI.PRIMARY }}">
|
||||||
<a href="{{ args.confirm_url }}" target="_blank"
|
<a href="{{ args.confirm_url }}" target="_blank"
|
||||||
style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 20px 50px; border-radius: 4px; border: 1px solid #530EEC; display: inline-block;">
|
style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 20px 50px; border-radius: 4px; border: 1px solid {{ UI.PRIMARY }}; display: inline-block;">
|
||||||
Confirm Email
|
Confirm Email
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
@ -24,7 +24,9 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="margin: 0 0 30px; font-size: 12px; text-align: center;">
|
<p style="margin: 0 0 30px; font-size: 12px; text-align: center;">
|
||||||
<a href="{{ args.confirm_url }}" target="_blank" style="color: #530EEC;">{{ args.confirm_url }}</a>
|
<a href="{{ args.confirm_url }}" target="_blank" style="color: {{ UI.PRIMARY }};">
|
||||||
|
{{ args.confirm_url }}
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="margin: 0; font-size: 10px; color: #AAA; text-align: center;">
|
<p style="margin: 0; font-size: 10px; color: #AAA; text-align: center;">
|
||||||
|
|
|
@ -2,13 +2,13 @@
|
||||||
You’ve been invited by <strong>{{ args.inviter.display_name }}</strong> to
|
You’ve been invited by <strong>{{ args.inviter.display_name }}</strong> to
|
||||||
join the team for
|
join the team for
|
||||||
<strong>{{ args.proposal.title or '<em>Untitled Project</em>'|safe }}</strong
|
<strong>{{ args.proposal.title or '<em>Untitled Project</em>'|safe }}</strong
|
||||||
>, a project on Grant.io! If you want to accept the invitation, continue to
|
>, a project on {{ UI.NAME }}! If you want to accept the invitation, continue to
|
||||||
the site below.
|
the site below.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{% if not args.user %}
|
{% if not args.user %}
|
||||||
<p style="margin: 20px 0 0;">
|
<p style="margin: 20px 0 0;">
|
||||||
It looks like you don't yet have a Grant.io account, so you'll need to sign up
|
It looks like you don't yet have a {{ UI.NAME }} account, so you'll need to sign up
|
||||||
first before you can join the team.
|
first before you can join the team.
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -18,11 +18,11 @@
|
||||||
<td bgcolor="#ffffff" align="center" style="padding: 40px 30px 0 30px;">
|
<td bgcolor="#ffffff" align="center" style="padding: 40px 30px 0 30px;">
|
||||||
<table border="0" cellspacing="0" cellpadding="0">
|
<table border="0" cellspacing="0" cellpadding="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="border-radius: 3px;" bgcolor="#530EEC">
|
<td align="center" style="border-radius: 3px;" bgcolor="{{ UI.PRIMARY }}">
|
||||||
<a
|
<a
|
||||||
href="{{ args.invite_url }}"
|
href="{{ args.invite_url }}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 20px 50px; border-radius: 4px; border: 1px solid #530EEC; display: inline-block;"
|
style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 20px 50px; border-radius: 4px; border: 1px solid {{ UI.PRIMARY }}; display: inline-block;"
|
||||||
>
|
>
|
||||||
{% if args.user %} See invitation {% else %} Get started {% endif %}
|
{% if args.user %} See invitation {% else %} Get started {% endif %}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
You’ve been invited by {{ args.inviter.display_name }} to join the team for
|
You’ve been invited by {{ args.inviter.display_name }} to join the team for
|
||||||
{{ args.proposal.title or 'Untitled Project' }}, a project on Grant.io! If
|
{{ args.proposal.title or 'Untitled Project' }}, a project on {{ UI.NAME }}! If
|
||||||
you want to accept the invitation, continue to the URL below.
|
you want to accept the invitation, continue to the URL below.
|
||||||
|
|
||||||
{{ args.invite_url }}
|
{{ args.invite_url }}
|
||||||
|
|
||||||
{% if not args.user %}
|
{% if not args.user %}
|
||||||
It looks like you don't yet have a Grant.io account, so you'll need to sign
|
It looks like you don't yet have a {{ UI.NAME }} account, so you'll need to sign
|
||||||
up first before you can join the team.
|
up first before you can join the team.
|
||||||
{% endif %}
|
{% endif %}
|
|
@ -81,7 +81,7 @@
|
||||||
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||||
<!-- LOGO -->
|
<!-- LOGO -->
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" bgcolor="#530EEC">
|
<td align="center" bgcolor="{{ UI.SECONDARY }}">
|
||||||
<!--[if (gte mso 9)|(IE)]>
|
<!--[if (gte mso 9)|(IE)]>
|
||||||
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
|
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -91,9 +91,9 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="padding: 40px 10px 40px 10px;" valign="top">
|
<td align="center" style="padding: 40px 10px 40px 10px;" valign="top">
|
||||||
<a href="{{ args.home_url }}" target="_blank">
|
<a href="{{ args.home_url }}" target="_blank">
|
||||||
<img alt="Logo" border="0" height="44" src="https://i.imgur.com/t0DPkyl.png"
|
<img alt="Logo" border="0" height="44" src="https://i.imgur.com/WIuJxYB.png"
|
||||||
style="display: block; width: 150px; max-width: 150px; min-width: 150px; font-family: 'Nunito Sans', Helvetica, Arial, sans-serif; color: #ffffff; font-size: 18px;"
|
style="display: block; width: 180px; max-width: 180px; min-width: 180px; font-family: 'Nunito Sans', Helvetica, Arial, sans-serif; color: #ffffff; font-size: 18px;"
|
||||||
width="120">
|
width="180">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -107,7 +107,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
<!-- TITLE -->
|
<!-- TITLE -->
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" bgcolor="#530EEC" style="padding: 0px 10px 0px 10px;">
|
<td align="center" bgcolor="{{ UI.SECONDARY }}" style="padding: 0px 10px 0px 10px;">
|
||||||
<!--[if (gte mso 9)|(IE)]>
|
<!--[if (gte mso 9)|(IE)]>
|
||||||
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
|
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -167,7 +167,9 @@
|
||||||
<td align="center" bgcolor="#f4f4f4"
|
<td align="center" bgcolor="#f4f4f4"
|
||||||
style="padding: 30px 30px 30px 30px; color: #666666; font-family: 'Nunito Sans', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; line-height: 18px;">
|
style="padding: 30px 30px 30px 30px; color: #666666; font-family: 'Nunito Sans', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; line-height: 18px;">
|
||||||
<p style="margin: 0;">
|
<p style="margin: 0;">
|
||||||
<a href="{{ args.home_url }}" style="color: #221F1F; font-weight: 700;" target="_blank">Grant.io</a>
|
<a href="{{ args.home_url }}" style="color: #221F1F; font-weight: 700;" target="_blank">
|
||||||
|
{{ UI.NAME }}
|
||||||
|
</a>
|
||||||
-
|
-
|
||||||
<a href="{{ args.account_url }}" style="color: #221F1F; font-weight: 700;" target="_blank">Your
|
<a href="{{ args.account_url }}" style="color: #221F1F; font-weight: 700;" target="_blank">Your
|
||||||
Account</a> -
|
Account</a> -
|
||||||
|
@ -195,7 +197,7 @@
|
||||||
<td align="center" bgcolor="#f4f4f4"
|
<td align="center" bgcolor="#f4f4f4"
|
||||||
style="padding: 0px 30px 30px 30px; color: #AAAAAA; font-family: 'Nunito Sans', Helvetica, Arial, sans-serif; font-size: 12px; font-weight: 400; line-height: 18px;">
|
style="padding: 0px 30px 30px 30px; color: #AAAAAA; font-family: 'Nunito Sans', Helvetica, Arial, sans-serif; font-size: 12px; font-weight: 400; line-height: 18px;">
|
||||||
<p style="margin: 0;">
|
<p style="margin: 0;">
|
||||||
Grant.io Inc, 123 Address Street, Somewhere, NY 11211
|
Zcash Foundation, 123 Address Street, Somewhere, NY 11211
|
||||||
</p>
|
</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
===============
|
===============
|
||||||
|
|
||||||
Grant.io
|
{{ UI.NAME }}
|
||||||
123 Address Street
|
123 Address Street
|
||||||
City, ST 12345
|
City, ST 12345
|
||||||
|
|
||||||
Unsubscribe here: https://grant.io/unsubscribe
|
Unsubscribe here: {{ args.unsubscribe_url }}
|
|
@ -180,6 +180,33 @@ class User(db.Model, UserMixin):
|
||||||
def set_password(self, password: str):
|
def set_password(self, password: str):
|
||||||
self.password = hash_password(password)
|
self.password = hash_password(password)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
send_email(self.email_address, 'change_password', {
|
||||||
|
'display_name': self.display_name,
|
||||||
|
'recover_url': make_url('/auth/recover'),
|
||||||
|
'contact_url': make_url('/contact')
|
||||||
|
})
|
||||||
|
|
||||||
|
def set_email(self, email: str):
|
||||||
|
# Update email address
|
||||||
|
old_email = self.email_address
|
||||||
|
self.email_address = email
|
||||||
|
# Delete old verification(s?)
|
||||||
|
old_evs = EmailVerification.query.filter_by(user_id=self.id).all()
|
||||||
|
for old_ev in old_evs:
|
||||||
|
db.session.delete(old_ev)
|
||||||
|
# Generate a new one
|
||||||
|
ev = EmailVerification(user_id=self.id)
|
||||||
|
db.session.add(ev)
|
||||||
|
# Save changes & send notification & verification emails
|
||||||
|
db.session.commit()
|
||||||
|
send_email(old_email, 'change_email_old', {
|
||||||
|
'display_name': self.display_name,
|
||||||
|
'contact_url': make_url('/contact')
|
||||||
|
})
|
||||||
|
send_email(self.email_address, 'change_email', {
|
||||||
|
'display_name': self.display_name,
|
||||||
|
'confirm_url': make_url(f'/email/verify?code={ev.code}')
|
||||||
|
})
|
||||||
|
|
||||||
def login(self):
|
def login(self):
|
||||||
login_user(self)
|
login_user(self)
|
||||||
|
|
|
@ -141,7 +141,7 @@ def auth_user(email, password):
|
||||||
return user_schema.dump(existing_user)
|
return user_schema.dump(existing_user)
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route("/password", methods=["PUT"])
|
@blueprint.route("/me/password", methods=["PUT"])
|
||||||
@requires_auth
|
@requires_auth
|
||||||
@endpoint.api(
|
@endpoint.api(
|
||||||
parameter('currentPassword', type=str, required=True),
|
parameter('currentPassword', type=str, required=True),
|
||||||
|
@ -154,6 +154,20 @@ def update_user_password(current_password, password):
|
||||||
return None, 200
|
return None, 200
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint.route("/me/email", methods=["PUT"])
|
||||||
|
@requires_auth
|
||||||
|
@endpoint.api(
|
||||||
|
parameter('email', type=str, required=True),
|
||||||
|
parameter('password', type=str, required=True)
|
||||||
|
)
|
||||||
|
def update_user_email(email, password):
|
||||||
|
if not g.current_user.check_password(password):
|
||||||
|
return {"message": "Password is incorrect"}, 403
|
||||||
|
print('set_email')
|
||||||
|
g.current_user.set_email(email)
|
||||||
|
return None, 200
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route("/logout", methods=["POST"])
|
@blueprint.route("/logout", methods=["POST"])
|
||||||
@requires_auth
|
@requires_auth
|
||||||
@endpoint.api()
|
@endpoint.api()
|
||||||
|
|
|
@ -2,47 +2,6 @@ import random
|
||||||
|
|
||||||
from grant.proposal.models import CATEGORIES
|
from grant.proposal.models import CATEGORIES
|
||||||
|
|
||||||
message = {
|
|
||||||
"sig": "0x7b3a85e9f158c2ae2a9ffba986a7dcb9108cf8ea9691080f80eadb506719f14925c89777aade3fabc5f9730ea389abdf7ffb0da16babdf1a1ea710b1e998cb891c",
|
|
||||||
"data": {
|
|
||||||
"domain": {
|
|
||||||
"name": "Grant.io",
|
|
||||||
"version": 1,
|
|
||||||
"chainId": 1543277948575
|
|
||||||
},
|
|
||||||
"types": {
|
|
||||||
"authorization": [
|
|
||||||
{
|
|
||||||
"name": "Message Proof",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Time",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"EIP712Domain": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "version",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "chainId",
|
|
||||||
"type": "uint256"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"message": "I am proving the identity of 0x6bEeA1Cef016c23e292381b6FcaeC092960e41aa on Grant.io",
|
|
||||||
"time": "Tue, 27 Nov 2018 19:02:04 GMT"
|
|
||||||
},
|
|
||||||
"primaryType": "authorization"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test_user = {
|
test_user = {
|
||||||
"displayName": 'Groot',
|
"displayName": 'Groot',
|
||||||
|
|
|
@ -96,7 +96,11 @@ export function updateUserPassword(
|
||||||
currentPassword: string,
|
currentPassword: string,
|
||||||
password: string,
|
password: string,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return axios.put(`/api/v1/users/password`, { currentPassword, password });
|
return axios.put(`/api/v1/users/me/password`, { currentPassword, password });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateUserEmail(email: string, password: string): Promise<any> {
|
||||||
|
return axios.put('/api/v1/users/me/email', { email, password });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateUser(user: User): Promise<{ data: User }> {
|
export function updateUser(user: User): Promise<{ data: User }> {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
.EmailSubscriptionsForm {
|
.EmailSubscriptionsForm {
|
||||||
max-width: 400px;
|
|
||||||
margin: 0 auto 2rem;
|
margin: 0 auto 2rem;
|
||||||
|
|
||||||
&-form {
|
&-form {
|
||||||
|
@ -7,6 +6,9 @@
|
||||||
margin-bottom: 0.1rem;
|
margin-bottom: 0.1rem;
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
|
> div:first-child .ant-divider {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
& .ant-form-item {
|
& .ant-form-item {
|
||||||
margin-bottom: -0.5rem;
|
margin-bottom: -0.5rem;
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
.PasswordFormItems {
|
|
||||||
&-confirm {
|
|
||||||
.ant-form-item-label label {
|
|
||||||
display: none; // hide label, keep geometry
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.ant-form-item {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,7 +3,6 @@ import classnames from 'classnames';
|
||||||
import { Form, Input, Row, Col } from 'antd';
|
import { Form, Input, Row, Col } from 'antd';
|
||||||
import { FormItemProps } from 'antd/lib/form';
|
import { FormItemProps } from 'antd/lib/form';
|
||||||
import { WrappedFormUtils } from 'antd/lib/form/Form';
|
import { WrappedFormUtils } from 'antd/lib/form/Form';
|
||||||
import './PasswordFormItems.less';
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
form: WrappedFormUtils;
|
form: WrappedFormUtils;
|
||||||
|
@ -31,8 +30,8 @@ export default class AddressInput extends React.Component<Props, State> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className="PasswordFormItems" gutter={8}>
|
<Row className="PasswordFormItems" gutter={8}>
|
||||||
<Col span={12}>
|
<Col sm={12} xs={24}>
|
||||||
<Form.Item {...formItemProps} label="Password">
|
<Form.Item {...formItemProps} label="New password">
|
||||||
{getFieldDecorator('password', {
|
{getFieldDecorator('password', {
|
||||||
rules: [
|
rules: [
|
||||||
{ required: true, message: 'Please enter a password' },
|
{ required: true, message: 'Please enter a password' },
|
||||||
|
@ -50,13 +49,13 @@ export default class AddressInput extends React.Component<Props, State> {
|
||||||
<Input
|
<Input
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="password"
|
placeholder="Minimum 8 chars"
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
/>,
|
/>,
|
||||||
)}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12} className="PasswordFormItems-confirm">
|
<Col sm={12} xs={24} className="PasswordFormItems-confirm">
|
||||||
<Form.Item {...formItemProps} label="Confirm password">
|
<Form.Item {...formItemProps} label="Confirm password">
|
||||||
{getFieldDecorator('passwordConfirm', {
|
{getFieldDecorator('passwordConfirm', {
|
||||||
rules: [
|
rules: [
|
||||||
|
@ -80,7 +79,7 @@ export default class AddressInput extends React.Component<Props, State> {
|
||||||
passwordConfirmDirty: passwordConfirmDirty || !!e.target.value,
|
passwordConfirmDirty: passwordConfirmDirty || !!e.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
placeholder="confirm password"
|
placeholder=""
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>,
|
/>,
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
.AccountSettings {
|
||||||
|
&-form {
|
||||||
|
& > .ant-form-item {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
& button {
|
||||||
|
margin: 1rem auto 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-alert {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,148 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Form, Input, Button, Alert } from 'antd';
|
||||||
|
import { FormComponentProps } from 'antd/lib/form';
|
||||||
|
import { updateUserEmail } from 'api/api';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import './Account.less';
|
||||||
|
|
||||||
|
interface StateProps {
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = FormComponentProps & StateProps;
|
||||||
|
|
||||||
|
const STATE = {
|
||||||
|
newEmail: '',
|
||||||
|
emailChangePending: false,
|
||||||
|
emailChangeSuccess: false,
|
||||||
|
emailChangeError: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = typeof STATE;
|
||||||
|
|
||||||
|
class AccountSettings extends React.Component<Props, State> {
|
||||||
|
state: State = { ...STATE };
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { email, form } = this.props;
|
||||||
|
const {
|
||||||
|
emailChangeError,
|
||||||
|
emailChangePending,
|
||||||
|
emailChangeSuccess,
|
||||||
|
newEmail,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="AccountSettings">
|
||||||
|
<Form
|
||||||
|
className="AccountSettings-form"
|
||||||
|
onSubmit={this.handleSubmit}
|
||||||
|
layout="vertical"
|
||||||
|
>
|
||||||
|
<Form.Item label="Email">
|
||||||
|
{form.getFieldDecorator('email', {
|
||||||
|
initialValue: newEmail || email,
|
||||||
|
rules: [
|
||||||
|
{ type: 'email', message: 'Please enter a valid email' },
|
||||||
|
{ required: true, message: 'Please enter a new email' },
|
||||||
|
],
|
||||||
|
})(
|
||||||
|
<Input
|
||||||
|
autoComplete="off"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="example@email.com"
|
||||||
|
/>,
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item label="Current password">
|
||||||
|
{form.getFieldDecorator('currentPassword', {
|
||||||
|
rules: [{ required: true, message: 'Please enter your current password' }],
|
||||||
|
})(
|
||||||
|
<Input
|
||||||
|
autoComplete="current-password"
|
||||||
|
name="currentPassword"
|
||||||
|
type="password"
|
||||||
|
placeholder="*********"
|
||||||
|
/>,
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
size="large"
|
||||||
|
block
|
||||||
|
disabled={form.getFieldValue('email') === email}
|
||||||
|
loading={emailChangePending}
|
||||||
|
>
|
||||||
|
Change email
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{emailChangeError && (
|
||||||
|
<Alert
|
||||||
|
type="error"
|
||||||
|
message={emailChangeError}
|
||||||
|
showIcon
|
||||||
|
closable
|
||||||
|
className="AccountSettings-alert"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{emailChangeSuccess && (
|
||||||
|
<Alert
|
||||||
|
type="success"
|
||||||
|
message="Email change successful!"
|
||||||
|
description="Check your email for a confirmation link."
|
||||||
|
showIcon
|
||||||
|
onClose={() => this.setState({ emailChangeSuccess: false })}
|
||||||
|
className="AccountSettings-alert"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleSubmit = (ev: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.props.form.validateFieldsAndScroll((err: any, values: any) => {
|
||||||
|
if (!err) {
|
||||||
|
this.setState({
|
||||||
|
emailChangePending: true,
|
||||||
|
emailChangeError: '',
|
||||||
|
emailChangeSuccess: false,
|
||||||
|
});
|
||||||
|
updateUserEmail(values.email, values.currentPassword)
|
||||||
|
.then(() => {
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
newEmail: values.email,
|
||||||
|
emailChangePending: false,
|
||||||
|
emailChangeSuccess: true,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
this.props.form.resetFields();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
this.setState({
|
||||||
|
emailChangePending: false,
|
||||||
|
emailChangeError: e.message || e.toSring(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormWrappedAccountSettings = Form.create()(AccountSettings);
|
||||||
|
|
||||||
|
export default connect<StateProps, {}, {}, AppState>(state => ({
|
||||||
|
email: state.auth.user ? state.auth.user.emailAddress || '' : '',
|
||||||
|
}))(FormWrappedAccountSettings);
|
|
@ -1,14 +1,11 @@
|
||||||
.ChangePassword {
|
.ChangePassword {
|
||||||
max-width: 400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
|
|
||||||
&-form {
|
&-form {
|
||||||
& > .ant-form-item {
|
& > .ant-form-item {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
& button {
|
& button {
|
||||||
margin: 0.6rem auto 0;
|
margin: 0.25rem auto 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,11 @@ class ChangePassword extends React.Component<Props, State> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ChangePassword">
|
<div className="ChangePassword">
|
||||||
<Form className="ChangePassword-form" onSubmit={this.handleSubmit}>
|
<Form
|
||||||
|
className="ChangePassword-form"
|
||||||
|
onSubmit={this.handleSubmit}
|
||||||
|
layout="vertical"
|
||||||
|
>
|
||||||
<Form.Item label="Current password">
|
<Form.Item label="Current password">
|
||||||
{getFieldDecorator('currentPassword', {
|
{getFieldDecorator('currentPassword', {
|
||||||
rules: [{ required: true, message: 'Please enter your current password' }],
|
rules: [{ required: true, message: 'Please enter your current password' }],
|
||||||
|
@ -37,7 +41,7 @@ class ChangePassword extends React.Component<Props, State> {
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
name="currentPassword"
|
name="currentPassword"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="current password"
|
placeholder="*********"
|
||||||
/>,
|
/>,
|
||||||
)}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
@ -69,7 +73,8 @@ class ChangePassword extends React.Component<Props, State> {
|
||||||
{passwordChangeSuccess && (
|
{passwordChangeSuccess && (
|
||||||
<Alert
|
<Alert
|
||||||
type="success"
|
type="success"
|
||||||
message="Password changed."
|
message="Password changed successfully!"
|
||||||
|
description="We’ve sent you an email to confirm this change."
|
||||||
showIcon
|
showIcon
|
||||||
closable
|
closable
|
||||||
onClose={() => this.setState({ passwordChangeSuccess: false })}
|
onClose={() => this.setState({ passwordChangeSuccess: false })}
|
||||||
|
|
|
@ -2,8 +2,16 @@
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
||||||
& > h1 {
|
.ant-tabs-content {
|
||||||
text-align: center;
|
min-height: 50vh;
|
||||||
font-size: 1.4rem;
|
}
|
||||||
|
|
||||||
|
// Only top-style at mobile
|
||||||
|
.ant-tabs-top {
|
||||||
|
margin: -2.5rem -2.5rem 0;
|
||||||
|
|
||||||
|
.ant-tabs-tabpane {
|
||||||
|
padding: 0 2.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { connect } from 'react-redux';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
import { Tabs } from 'antd';
|
import { Tabs } from 'antd';
|
||||||
import LinkableTabs from 'components/LinkableTabs';
|
import LinkableTabs from 'components/LinkableTabs';
|
||||||
|
import Account from './Account';
|
||||||
import ChangePassword from './ChangePassword';
|
import ChangePassword from './ChangePassword';
|
||||||
import EmailSubscriptions from './EmailSubscriptions';
|
import EmailSubscriptions from './EmailSubscriptions';
|
||||||
|
|
||||||
|
@ -15,25 +16,53 @@ interface StateProps {
|
||||||
|
|
||||||
type Props = StateProps;
|
type Props = StateProps;
|
||||||
|
|
||||||
class Settings extends React.Component<Props> {
|
interface State {
|
||||||
|
tabPosition: 'left' | 'top';
|
||||||
|
}
|
||||||
|
|
||||||
|
class Settings extends React.Component<Props, State> {
|
||||||
|
state: State = {
|
||||||
|
tabPosition: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
window.addEventListener('resize', this.handleResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
window.removeEventListener('resize', this.handleResize);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { authUser } = this.props;
|
const { authUser } = this.props;
|
||||||
if (!authUser) return null;
|
if (!authUser) return null;
|
||||||
|
const { tabPosition } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="Settings">
|
<div className="Settings">
|
||||||
<h1>Settings</h1>
|
<LinkableTabs defaultActiveKey="account" tabPosition={tabPosition}>
|
||||||
<LinkableTabs defaultActiveKey="password" tabPosition="top">
|
<TabPane tab="Account" key="account">
|
||||||
|
<Account />
|
||||||
|
</TabPane>
|
||||||
|
<TabPane tab="Notifications" key="emails">
|
||||||
|
<EmailSubscriptions />
|
||||||
|
</TabPane>
|
||||||
<TabPane tab="Change Password" key="password">
|
<TabPane tab="Change Password" key="password">
|
||||||
<ChangePassword />
|
<ChangePassword />
|
||||||
</TabPane>
|
</TabPane>
|
||||||
<TabPane tab="Email Notifications" key="emails">
|
|
||||||
<EmailSubscriptions />
|
|
||||||
</TabPane>
|
|
||||||
</LinkableTabs>
|
</LinkableTabs>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleResize = () => {
|
||||||
|
const { tabPosition } = this.state;
|
||||||
|
if (tabPosition === 'left' && window.innerWidth < 460) {
|
||||||
|
this.setState({ tabPosition: 'top' });
|
||||||
|
} else if (tabPosition === 'top' && window.innerWidth >= 460) {
|
||||||
|
this.setState({ tabPosition: 'left' });
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const withConnect = connect<StateProps, {}, {}, AppState>(state => ({
|
const withConnect = connect<StateProps, {}, {}, AppState>(state => ({
|
||||||
|
|
Loading…
Reference in New Issue