Merge pull request #105 from grant-project/change-email

Settings redesign + change email + change password conf email
This commit is contained in:
Daniel Ternyak 2019-01-27 16:10:53 -06:00 committed by GitHub
commit 236d5b1ccf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 499 additions and 151 deletions

View File

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

View File

@ -21,7 +21,7 @@ export default class Example extends React.Component<Props> {
<div className="Example-inbox-left">
<div className="Example-inbox-left-icon is-checkbox" />
<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 className="Example-inbox-subject">
<strong>{email.info.subject}</strong>

View File

@ -16,6 +16,21 @@ export default [
title: 'Password recovery',
description: 'For recovering a users 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',
title: 'Proposal team invite',

View File

@ -30,7 +30,7 @@ class Template extends React.Component<Props> {
</div>
)}
<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.Item key="/">
<Link to="/">

View File

@ -4,7 +4,7 @@
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Admin - Grant.io</title>
<title>Admin - ZF Grants</title>
</head>
<body>

View File

@ -1,7 +1,7 @@
# Environment variable overrides for local development
FLASK_APP=app.py
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"
REDISTOGO_URL="redis://localhost:6379"
SECRET_KEY="not-so-secret"

View File

@ -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
@ -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"
## 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
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. 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. set **Callback URLs** to `http://127.0.0.1:3000/callback/twitter`
1. fill out other required fields

View File

@ -35,6 +35,7 @@ update = FakeUpdate()
example_email_args = {
'signup': {
'display_name': user.display_name,
'confirm_url': 'http://someconfirmurl.com',
},
'team_invite': {
@ -45,6 +46,19 @@ example_email_args = {
'recover': {
'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': proposal,
'proposal_url': 'http://someproposal.com',

View File

@ -1,24 +1,25 @@
import sendgrid
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 sendgrid.helpers.mail import Email, Mail, Content
from .subscription_settings import EmailSubscription, is_subscribed
default_template_args = {
'home_url': 'https://grant.io',
'account_url': 'https://grant.io/user',
'email_settings_url': 'https://grant.io/user/settings',
'unsubscribe_url': 'https://grant.io/unsubscribe',
'home_url': make_url('/'),
'account_url': make_url('/user'),
'email_settings_url': make_url('/settings'),
'unsubscribe_url': make_url('/unsubscribe'),
}
def signup_info(email_args):
return {
'subject': 'Confirm your email on Grant.io',
'title': 'Welcome to Grant.io!',
'preview': 'Welcome to Grant.io, we just need to confirm your email address.',
'subject': 'Confirm your email on {}'.format(UI['NAME']),
'title': 'Welcome to {}!'.format(UI['NAME']),
'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):
return {
'subject': 'Grant.io account recovery',
'title': 'Grant.io account recovery',
'subject': '{} account recovery'.format(UI['NAME']),
'title': '{} account recovery'.format(UI['NAME']),
'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):
return {
'subject': 'Your proposal has been approved!',
@ -118,6 +143,9 @@ get_info_lookup = {
'signup': signup_info,
'team_invite': team_invite_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_rejected': proposal_rejected,
'proposal_contribution': proposal_contribution,
@ -130,19 +158,35 @@ get_info_lookup = {
def generate_email(type, email_args):
info = get_info_lookup[type](email_args)
body_text = render_template('emails/%s.txt' % (type), args=email_args)
body_html = render_template('emails/%s.html' % (type), args=email_args)
body_text = render_template(
'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={
**default_template_args,
**info,
'body': Markup(body_html),
})
text = render_template('emails/template.txt', args={
**default_template_args,
**info,
'body': body_text,
})
html = render_template(
'emails/template.html',
args={
**default_template_args,
**info,
'body': Markup(body_html),
},
UI=UI,
)
text = render_template(
'emails/template.txt',
args={
**default_template_args,
**info,
'body': body_text,
},
UI=UI,
)
return {
'info': info,

View File

@ -13,7 +13,7 @@ env.read_env()
ENV = env.str("FLASK_ENV", default="production")
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")
QUEUES = ["default"]
SECRET_KEY = env.str("SECRET_KEY")
@ -24,7 +24,7 @@ CACHE_TYPE = "simple" # Can be "memcached", "redis", etc.
SQLALCHEMY_TRACK_MODIFICATIONS = False
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_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")
ADMIN_PASS_HASH = env.str("ADMIN_PASS_HASH")
UI = {
'NAME': 'ZF Grants',
'PRIMARY': '#CF8A00',
'SECONDARY': '#2D2A26',
}

View File

@ -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;">
Dont know why you got this email? Dont worry, you can safely ignore it. We wont send you anymore.
</p>

View File

@ -0,0 +1,5 @@
Hey {{ args.display_name }}, you just changed your email. In order to confirm this change, youll need to visit the link below.
{{ args.confirm_url }}
Dont know why you got this email? Dont worry, you can safely ignore it. We wont send you anymore.

View File

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

View File

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

View File

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

View File

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

View File

@ -13,9 +13,9 @@
<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="#530EEC">
<td align="center" style="border-radius: 3px;" bgcolor="{{ UI.PRIMARY }}">
<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
</a>
</td>

View File

@ -15,11 +15,11 @@
<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="#530EEC">
<td align="center" style="border-radius: 3px;" bgcolor="{{ UI.PRIMARY }}">
<a
href="{{ args.update_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 the full update
</a>

View File

@ -18,11 +18,11 @@
<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="#530EEC">
<td align="center" style="border-radius: 3px;" bgcolor="{{ UI.PRIMARY }}">
<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;"
>
Publish your proposal
</a>

View File

@ -12,9 +12,9 @@
<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="#530EEC">
<td align="center" style="border-radius: 3px;" bgcolor="{{ UI.PRIMARY }}">
<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
</a>
</td>

View File

@ -11,9 +11,9 @@
<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="#530EEC">
<td align="center" style="border-radius: 3px;" bgcolor="{{ UI.PRIMARY }}">
<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
</a>
</td>

View File

@ -8,11 +8,11 @@
<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="#530EEC">
<td align="center" style="border-radius: 3px;" bgcolor="{{ UI.PRIMARY }}">
<a
href="{{ args.recover_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;"
>
Reset Password
</a>
@ -28,7 +28,7 @@
</p>
<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
}}</a>
</p>

View File

@ -7,9 +7,9 @@
<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="#530EEC">
<td align="center" style="border-radius: 3px;" bgcolor="{{ UI.PRIMARY }}">
<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
</a>
</td>
@ -24,7 +24,9 @@
</p>
<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 style="margin: 0; font-size: 10px; color: #AAA; text-align: center;">

View File

@ -2,13 +2,13 @@
Youve been invited by <strong>{{ args.inviter.display_name }}</strong> to
join the team for
<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.
</p>
{% if not args.user %}
<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.
</p>
{% endif %}
@ -18,11 +18,11 @@
<td bgcolor="#ffffff" align="center" style="padding: 40px 30px 0 30px;">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="border-radius: 3px;" bgcolor="#530EEC">
<td align="center" style="border-radius: 3px;" bgcolor="{{ UI.PRIMARY }}">
<a
href="{{ args.invite_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;"
>
{% if args.user %} See invitation {% else %} Get started {% endif %}
</a>

View File

@ -1,10 +1,10 @@
Youve 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.
{{ args.invite_url }}
{% 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.
{% endif %}

View File

@ -81,7 +81,7 @@
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<!-- LOGO -->
<tr>
<td align="center" bgcolor="#530EEC">
<td align="center" bgcolor="{{ UI.SECONDARY }}">
<!--[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
<tr>
@ -91,9 +91,9 @@
<tr>
<td align="center" style="padding: 40px 10px 40px 10px;" valign="top">
<a href="{{ args.home_url }}" target="_blank">
<img alt="Logo" border="0" height="44" src="https://i.imgur.com/t0DPkyl.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;"
width="120">
<img alt="Logo" border="0" height="44" src="https://i.imgur.com/WIuJxYB.png"
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="180">
</a>
</td>
</tr>
@ -107,7 +107,7 @@
</tr>
<!-- TITLE -->
<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)]>
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
<tr>
@ -167,7 +167,9 @@
<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;">
<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
Account</a> -
@ -195,7 +197,7 @@
<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;">
<p style="margin: 0;">
Grant.io Inc, 123 Address Street, Somewhere, NY 11211
Zcash Foundation, 123 Address Street, Somewhere, NY 11211
</p>
</td>
</tr>

View File

@ -2,8 +2,8 @@
===============
Grant.io
{{ UI.NAME }}
123 Address Street
City, ST 12345
Unsubscribe here: https://grant.io/unsubscribe
Unsubscribe here: {{ args.unsubscribe_url }}

View File

@ -180,6 +180,33 @@ class User(db.Model, UserMixin):
def set_password(self, password: str):
self.password = hash_password(password)
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):
login_user(self)

View File

@ -141,7 +141,7 @@ def auth_user(email, password):
return user_schema.dump(existing_user)
@blueprint.route("/password", methods=["PUT"])
@blueprint.route("/me/password", methods=["PUT"])
@requires_auth
@endpoint.api(
parameter('currentPassword', type=str, required=True),
@ -154,6 +154,20 @@ def update_user_password(current_password, password):
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"])
@requires_auth
@endpoint.api()

View File

@ -2,47 +2,6 @@ import random
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 = {
"displayName": 'Groot',

View File

@ -96,7 +96,11 @@ export function updateUserPassword(
currentPassword: string,
password: string,
): 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 }> {

View File

@ -1,5 +1,4 @@
.EmailSubscriptionsForm {
max-width: 400px;
margin: 0 auto 2rem;
&-form {
@ -7,6 +6,9 @@
margin-bottom: 0.1rem;
margin-top: 2rem;
}
> div:first-child .ant-divider {
margin-top: 0;
}
& .ant-form-item {
margin-bottom: -0.5rem;

View File

@ -1,10 +0,0 @@
.PasswordFormItems {
&-confirm {
.ant-form-item-label label {
display: none; // hide label, keep geometry
}
}
.ant-form-item {
margin-bottom: 0.5rem;
}
}

View File

@ -3,7 +3,6 @@ import classnames from 'classnames';
import { Form, Input, Row, Col } from 'antd';
import { FormItemProps } from 'antd/lib/form';
import { WrappedFormUtils } from 'antd/lib/form/Form';
import './PasswordFormItems.less';
export interface Props {
form: WrappedFormUtils;
@ -31,8 +30,8 @@ export default class AddressInput extends React.Component<Props, State> {
return (
<Row className="PasswordFormItems" gutter={8}>
<Col span={12}>
<Form.Item {...formItemProps} label="Password">
<Col sm={12} xs={24}>
<Form.Item {...formItemProps} label="New password">
{getFieldDecorator('password', {
rules: [
{ required: true, message: 'Please enter a password' },
@ -50,13 +49,13 @@ export default class AddressInput extends React.Component<Props, State> {
<Input
name="password"
type="password"
placeholder="password"
placeholder="Minimum 8 chars"
autoComplete="new-password"
/>,
)}
</Form.Item>
</Col>
<Col span={12} className="PasswordFormItems-confirm">
<Col sm={12} xs={24} className="PasswordFormItems-confirm">
<Form.Item {...formItemProps} label="Confirm password">
{getFieldDecorator('passwordConfirm', {
rules: [
@ -80,7 +79,7 @@ export default class AddressInput extends React.Component<Props, State> {
passwordConfirmDirty: passwordConfirmDirty || !!e.target.value,
})
}
placeholder="confirm password"
placeholder=""
autoComplete="off"
/>,
)}

View File

@ -0,0 +1,15 @@
.AccountSettings {
&-form {
& > .ant-form-item {
margin-bottom: 0.75rem;
}
& button {
margin: 1rem auto 0;
}
}
&-alert {
margin-top: 1rem;
}
}

View File

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

View File

@ -1,14 +1,11 @@
.ChangePassword {
max-width: 400px;
margin: 0 auto;
&-form {
& > .ant-form-item {
margin-bottom: 0.5rem;
margin-bottom: 0.75rem;
}
& button {
margin: 0.6rem auto 0;
margin: 0.25rem auto 0;
}
}

View File

@ -28,7 +28,11 @@ class ChangePassword extends React.Component<Props, State> {
return (
<div className="ChangePassword">
<Form className="ChangePassword-form" onSubmit={this.handleSubmit}>
<Form
className="ChangePassword-form"
onSubmit={this.handleSubmit}
layout="vertical"
>
<Form.Item label="Current password">
{getFieldDecorator('currentPassword', {
rules: [{ required: true, message: 'Please enter your current password' }],
@ -37,7 +41,7 @@ class ChangePassword extends React.Component<Props, State> {
autoComplete="current-password"
name="currentPassword"
type="password"
placeholder="current password"
placeholder="*********"
/>,
)}
</Form.Item>
@ -69,7 +73,8 @@ class ChangePassword extends React.Component<Props, State> {
{passwordChangeSuccess && (
<Alert
type="success"
message="Password changed."
message="Password changed successfully!"
description="Weve sent you an email to confirm this change."
showIcon
closable
onClose={() => this.setState({ passwordChangeSuccess: false })}

View File

@ -2,8 +2,16 @@
max-width: 800px;
margin: 0 auto;
& > h1 {
text-align: center;
font-size: 1.4rem;
.ant-tabs-content {
min-height: 50vh;
}
// Only top-style at mobile
.ant-tabs-top {
margin: -2.5rem -2.5rem 0;
.ant-tabs-tabpane {
padding: 0 2.5rem;
}
}
}

View File

@ -4,6 +4,7 @@ import { connect } from 'react-redux';
import './index.less';
import { Tabs } from 'antd';
import LinkableTabs from 'components/LinkableTabs';
import Account from './Account';
import ChangePassword from './ChangePassword';
import EmailSubscriptions from './EmailSubscriptions';
@ -15,25 +16,53 @@ interface 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() {
const { authUser } = this.props;
if (!authUser) return null;
const { tabPosition } = this.state;
return (
<div className="Settings">
<h1>Settings</h1>
<LinkableTabs defaultActiveKey="password" tabPosition="top">
<LinkableTabs defaultActiveKey="account" tabPosition={tabPosition}>
<TabPane tab="Account" key="account">
<Account />
</TabPane>
<TabPane tab="Notifications" key="emails">
<EmailSubscriptions />
</TabPane>
<TabPane tab="Change Password" key="password">
<ChangePassword />
</TabPane>
<TabPane tab="Email Notifications" key="emails">
<EmailSubscriptions />
</TabPane>
</LinkableTabs>
</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 => ({