Merge pull request #404 from grant-project/better-validation
Better validation
This commit is contained in:
commit
1acbcc9675
|
@ -32,5 +32,8 @@ BLOCKCHAIN_API_SECRET="ef0b48e41f78d3ae85b1379b386f1bca"
|
|||
# EXPLORER_URL="https://chain.so/tx/ZEC/<txid>"
|
||||
EXPLORER_URL="https://chain.so/tx/ZECTEST/<txid>"
|
||||
|
||||
# Amount for staking a proposal in ZEC
|
||||
# Amount for staking a proposal in ZEC, keep in sync with frontend .env
|
||||
PROPOSAL_STAKING_AMOUNT=0.025
|
||||
|
||||
# Maximum amount for a proposal target, keep in sync with frontend .env
|
||||
PROPOSAL_TARGET_MAX=10000
|
||||
|
|
|
@ -30,7 +30,7 @@ class Comment(db.Model):
|
|||
self.proposal_id = proposal_id
|
||||
self.user_id = user_id
|
||||
self.parent_comment_id = parent_comment_id
|
||||
self.content = content
|
||||
self.content = content[:1000]
|
||||
self.date_created = datetime.datetime.now()
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -53,11 +53,11 @@ class Milestone(db.Model):
|
|||
proposal_id=int,
|
||||
):
|
||||
self.id = gen_random_id(Milestone)
|
||||
self.title = title
|
||||
self.content = content
|
||||
self.title = title[:255]
|
||||
self.content = content[:255]
|
||||
self.stage = stage
|
||||
self.date_estimated = date_estimated
|
||||
self.payout_percent = payout_percent
|
||||
self.payout_percent = payout_percent[:255]
|
||||
self.immediate_payout = immediate_payout
|
||||
self.proposal_id = proposal_id
|
||||
self.date_created = datetime.datetime.now()
|
||||
|
@ -70,21 +70,16 @@ class Milestone(db.Model):
|
|||
[db.session.delete(x) for x in proposal.milestones]
|
||||
for i, milestone_data in enumerate(milestones_data):
|
||||
m = Milestone(
|
||||
title=milestone_data["title"],
|
||||
content=milestone_data["content"],
|
||||
title=milestone_data["title"][:255],
|
||||
content=milestone_data["content"][:255],
|
||||
date_estimated=datetime.datetime.fromtimestamp(milestone_data["date_estimated"]),
|
||||
payout_percent=str(milestone_data["payout_percent"]),
|
||||
payout_percent=str(milestone_data["payout_percent"])[:255],
|
||||
immediate_payout=milestone_data["immediate_payout"],
|
||||
proposal_id=proposal.id,
|
||||
index=i
|
||||
)
|
||||
db.session.add(m)
|
||||
|
||||
@staticmethod
|
||||
def validate(milestone):
|
||||
if len(milestone.title) > 60:
|
||||
raise ValidationException("Milestone title must be no more than 60 chars")
|
||||
|
||||
def request_payout(self, user_id: int):
|
||||
if self.stage not in [MilestoneStage.IDLE, MilestoneStage.REJECTED]:
|
||||
raise MilestoneException(f'Cannot request payout for milestone at {self.stage} stage')
|
||||
|
|
|
@ -11,7 +11,7 @@ from flask import current_app
|
|||
from grant.comment.models import Comment
|
||||
from grant.email.send import send_email
|
||||
from grant.extensions import ma, db
|
||||
from grant.settings import PROPOSAL_STAKING_AMOUNT
|
||||
from grant.settings import PROPOSAL_STAKING_AMOUNT, PROPOSAL_TARGET_MAX
|
||||
from grant.task.jobs import ContributionExpired
|
||||
from grant.utils.enums import (
|
||||
ProposalStatus,
|
||||
|
@ -45,7 +45,7 @@ class ProposalTeamInvite(db.Model):
|
|||
|
||||
def __init__(self, proposal_id: int, address: str, accepted: bool = None):
|
||||
self.proposal_id = proposal_id
|
||||
self.address = address
|
||||
self.address = address[:255]
|
||||
self.accepted = accepted
|
||||
self.date_created = datetime.datetime.now()
|
||||
|
||||
|
@ -70,7 +70,7 @@ class ProposalUpdate(db.Model):
|
|||
def __init__(self, proposal_id: int, title: str, content: str):
|
||||
self.id = gen_random_id(ProposalUpdate)
|
||||
self.proposal_id = proposal_id
|
||||
self.title = title
|
||||
self.title = title[:255]
|
||||
self.content = content
|
||||
self.date_created = datetime.datetime.now()
|
||||
|
||||
|
@ -275,12 +275,11 @@ class Proposal(db.Model):
|
|||
|
||||
@staticmethod
|
||||
def simple_validate(proposal):
|
||||
title = proposal.get('title')
|
||||
# Validate fields to be database save-able.
|
||||
# Stricter validation is done in validate_publishable.
|
||||
stage = proposal.get('stage')
|
||||
category = proposal.get('category')
|
||||
|
||||
if title and len(title) > 60:
|
||||
raise ValidationException("Proposal title cannot be longer than 60 characters")
|
||||
if stage and not ProposalStage.includes(stage):
|
||||
raise ValidationException("Proposal stage {} is not a valid stage".format(stage))
|
||||
if category and not Category.includes(category):
|
||||
|
@ -294,37 +293,59 @@ class Proposal(db.Model):
|
|||
raise ValidationException("Only the first milestone can have an immediate payout")
|
||||
|
||||
if len(milestone.title) > 60:
|
||||
raise ValidationException("Milestone title must be no more than 60 chars")
|
||||
raise ValidationException("Milestone title cannot be longer than 60 chars")
|
||||
|
||||
if len(milestone.content) > 200:
|
||||
raise ValidationException("Milestone content must be no more than 200 chars")
|
||||
raise ValidationException("Milestone content cannot be longer than 200 chars")
|
||||
|
||||
payout_total += float(milestone.payout_percent)
|
||||
try:
|
||||
p = float(milestone.payout_percent)
|
||||
if not p.is_integer():
|
||||
raise ValidationException("Milestone payout percents must be whole numbers, no decimals")
|
||||
if p <= 0 or p > 100:
|
||||
raise ValidationException("Milestone payout percent must be greater than zero")
|
||||
except ValueError:
|
||||
raise ValidationException("Milestone payout percent must be a number")
|
||||
|
||||
payout_total += p
|
||||
|
||||
try:
|
||||
present = datetime.datetime.today().replace(day=1)
|
||||
if present > milestone.date_estimated:
|
||||
raise ValidationException("Milestone date_estimated must be in the future ")
|
||||
raise ValidationException("Milestone date estimate must be in the future ")
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.warn(
|
||||
f"Unexpected validation error - client prohibits {e}"
|
||||
)
|
||||
raise ValidationException("date_estimated does not convert to a datetime")
|
||||
raise ValidationException("Date estimate is not a valid datetime")
|
||||
|
||||
if payout_total != 100.0:
|
||||
raise ValidationException("payoutPercent across milestones must sum to exactly 100")
|
||||
raise ValidationException("Payout percentages of milestones must add up to exactly 100%")
|
||||
|
||||
def validate_publishable(self):
|
||||
self.validate_publishable_milestones()
|
||||
|
||||
# Require certain fields
|
||||
|
||||
required_fields = ['title', 'content', 'brief', 'category', 'target', 'payout_address']
|
||||
for field in required_fields:
|
||||
if not hasattr(self, field):
|
||||
raise ValidationException("Proposal must have a {}".format(field))
|
||||
|
||||
# Stricter limits on certain fields
|
||||
if len(self.title) > 60:
|
||||
raise ValidationException("Proposal title cannot be longer than 60 characters")
|
||||
if len(self.brief) > 140:
|
||||
raise ValidationException("Brief cannot be longer than 140 characters")
|
||||
if len(self.content) > 250000:
|
||||
raise ValidationException("Content cannot be longer than 250,000 characters")
|
||||
if Decimal(self.target) > PROPOSAL_TARGET_MAX:
|
||||
raise ValidationException("Target cannot be more than {} ZEC".format(PROPOSAL_TARGET_MAX))
|
||||
if Decimal(self.target) < 0.0001:
|
||||
raise ValidationException("Target cannot be less than 0.0001")
|
||||
if self.deadline_duration > 7776000:
|
||||
raise ValidationException("Deadline duration cannot be more than 90 days")
|
||||
|
||||
# Check with node that the address is kosher
|
||||
try:
|
||||
res = blockchain_get('/validate/address', {'address': self.payout_address})
|
||||
|
@ -380,12 +401,12 @@ class Proposal(db.Model):
|
|||
payout_address: str = '',
|
||||
deadline_duration: int = 5184000 # 60 days
|
||||
):
|
||||
self.title = title
|
||||
self.brief = brief
|
||||
self.title = title[:255]
|
||||
self.brief = brief[:255]
|
||||
self.category = category
|
||||
self.content = content
|
||||
self.target = target if target != '' else None
|
||||
self.payout_address = payout_address
|
||||
self.content = content[:300000]
|
||||
self.target = target[:255] if target != '' else None
|
||||
self.payout_address = payout_address[:255]
|
||||
self.deadline_duration = deadline_duration
|
||||
Proposal.simple_validate(vars(self))
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ from flask import Blueprint, g, request, current_app
|
|||
from marshmallow import fields, validate
|
||||
from sqlalchemy import or_
|
||||
from sentry_sdk import capture_message
|
||||
from webargs import validate
|
||||
|
||||
from grant.extensions import limiter
|
||||
from grant.comment.models import Comment, comment_schema, comments_schema
|
||||
|
@ -98,7 +99,7 @@ def report_proposal_comment(proposal_id, comment_id):
|
|||
@limiter.limit("30/hour;2/minute")
|
||||
@requires_email_verified_auth
|
||||
@body({
|
||||
"comment": fields.Str(required=True),
|
||||
"comment": fields.Str(required=True, validate=validate.Length(max=1000)),
|
||||
"parentCommentId": fields.Int(required=False, missing=None),
|
||||
})
|
||||
def post_proposal_comments(proposal_id, comment, parent_comment_id):
|
||||
|
@ -122,9 +123,6 @@ def post_proposal_comments(proposal_id, comment, parent_comment_id):
|
|||
if g.current_user.silenced:
|
||||
return {"message": "Your account has been silenced, commenting is disabled."}, 403
|
||||
|
||||
if len(comment) > 1000:
|
||||
return {"message": "Please make sure your comment is less than 1000 characters long"}, 400
|
||||
|
||||
# Make the comment
|
||||
comment = Comment(
|
||||
proposal_id=proposal_id,
|
||||
|
@ -218,6 +216,7 @@ def get_proposal_drafts():
|
|||
@blueprint.route("/<proposal_id>", methods=["PUT"])
|
||||
@requires_team_member_auth
|
||||
@body({
|
||||
# Length checks are to prevent database errors, not actual user limits imposed
|
||||
"title": fields.Str(required=True),
|
||||
"brief": fields.Str(required=True),
|
||||
"category": fields.Str(required=True, validate=validate.OneOf(choices=Category.list() + [''])),
|
||||
|
@ -226,7 +225,7 @@ def get_proposal_drafts():
|
|||
"payoutAddress": fields.Str(required=True),
|
||||
"deadlineDuration": fields.Int(required=True),
|
||||
"milestones": fields.List(fields.Dict(), required=True),
|
||||
"rfpOptIn": fields.Bool(required=False, missing=None)
|
||||
"rfpOptIn": fields.Bool(required=False, missing=None),
|
||||
})
|
||||
def update_proposal(milestones, proposal_id, rfp_opt_in, **kwargs):
|
||||
# Update the base proposal fields
|
||||
|
@ -348,8 +347,8 @@ def get_proposal_update(proposal_id, update_id):
|
|||
@limiter.limit("5/day;1/minute")
|
||||
@requires_team_member_auth
|
||||
@body({
|
||||
"title": fields.Str(required=True, validate=lambda p: 3 <= len(p) <= 30),
|
||||
"content": fields.Str(required=True, validate=lambda p: 5 <= len(p) <= 10000),
|
||||
"title": fields.Str(required=True, validate=validate.Length(min=3, max=60)),
|
||||
"content": fields.Str(required=True, validate=validate.Length(min=5, max=10000)),
|
||||
})
|
||||
def post_proposal_update(proposal_id, title, content):
|
||||
update = ProposalUpdate(
|
||||
|
@ -376,7 +375,7 @@ def post_proposal_update(proposal_id, title, content):
|
|||
@limiter.limit("30/day;10/minute")
|
||||
@requires_team_member_auth
|
||||
@body({
|
||||
"address": fields.Str(required=True),
|
||||
"address": fields.Str(required=True, validate=validate.Length(max=255)),
|
||||
})
|
||||
def post_proposal_team_invite(proposal_id, address):
|
||||
for u in g.current_proposal.team:
|
||||
|
@ -647,7 +646,7 @@ def accept_milestone_payout_request(proposal_id, milestone_id):
|
|||
@blueprint.route("/<proposal_id>/milestone/<milestone_id>/reject", methods=["PUT"])
|
||||
@requires_arbiter_auth
|
||||
@body({
|
||||
"reason": fields.Str(required=True, validate=lambda p: 2 <= len(p) <= 200),
|
||||
"reason": fields.Str(required=True, validate=validate.Length(min=2, max=200)),
|
||||
})
|
||||
def reject_milestone_payout_request(proposal_id, milestone_id, reason):
|
||||
if not g.current_proposal.is_funded:
|
||||
|
|
|
@ -64,8 +64,8 @@ class RFP(db.Model):
|
|||
assert Category.includes(category)
|
||||
self.id = gen_random_id(RFP)
|
||||
self.date_created = datetime.now()
|
||||
self.title = title
|
||||
self.brief = brief
|
||||
self.title = title[:255]
|
||||
self.brief = brief[:255]
|
||||
self.content = content
|
||||
self.category = category
|
||||
self.bounty = bounty
|
||||
|
|
|
@ -61,6 +61,7 @@ BLOCKCHAIN_API_SECRET = env.str("BLOCKCHAIN_API_SECRET")
|
|||
EXPLORER_URL = env.str("EXPLORER_URL", default="https://chain.so/tx/ZECTEST/<txid>")
|
||||
|
||||
PROPOSAL_STAKING_AMOUNT = Decimal(env.str("PROPOSAL_STAKING_AMOUNT"))
|
||||
PROPOSAL_TARGET_MAX = Decimal(env.str("PROPOSAL_TARGET_MAX"))
|
||||
|
||||
UI = {
|
||||
'NAME': 'ZF Grants',
|
||||
|
|
|
@ -46,8 +46,8 @@ class SocialMedia(db.Model):
|
|||
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||
|
||||
def __init__(self, service: str, username: str, user_id):
|
||||
self.service = service.upper()
|
||||
self.username = username.lower()
|
||||
self.service = service.upper()[:255]
|
||||
self.username = username.lower()[:255]
|
||||
self.user_id = user_id
|
||||
|
||||
|
||||
|
@ -145,8 +145,8 @@ class User(db.Model, UserMixin):
|
|||
):
|
||||
self.id = gen_random_id(User)
|
||||
self.email_address = email_address
|
||||
self.display_name = display_name
|
||||
self.title = title
|
||||
self.display_name = display_name[:255]
|
||||
self.title = title[:255]
|
||||
self.password = password
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -3,6 +3,7 @@ from animal_case import keys_to_snake_case
|
|||
from flask import Blueprint, g, current_app
|
||||
from marshmallow import fields
|
||||
from validate_email import validate_email
|
||||
from webargs import validate
|
||||
|
||||
import grant.utils.auth as auth
|
||||
from grant.comment.models import Comment, user_comments_schema
|
||||
|
@ -95,8 +96,8 @@ def get_user(user_id, with_proposals, with_comments, with_funded, with_pending,
|
|||
@body({
|
||||
"emailAddress": fields.Str(required=True, validate=lambda e: validate_email(e)),
|
||||
"password": fields.Str(required=True),
|
||||
"displayName": fields.Str(required=True, validate=lambda p: 2 <= len(p) <= 200),
|
||||
"title": fields.Str(required=True, validate=lambda p: 2 <= len(p) <= 200),
|
||||
"displayName": fields.Str(required=True, validate=validate.Length(min=2, max=50)),
|
||||
"title": fields.Str(required=True, validate=validate.Length(min=2, max=50)),
|
||||
})
|
||||
def create_user(
|
||||
email_address,
|
||||
|
|
|
@ -128,7 +128,6 @@ class TestProposalCommentAPI(BaseUserConfig):
|
|||
)
|
||||
|
||||
self.assertStatus(comment_res, 400)
|
||||
self.assertIn('less than', comment_res.json['message'])
|
||||
|
||||
def test_create_new_proposal_comment_fails_with_silenced_user(self):
|
||||
self.login_default_user()
|
||||
|
|
|
@ -16,7 +16,7 @@ BACKEND_URL=http://localhost:5000
|
|||
# EXPLORER_URL="https://chain.so/tx/ZEC/<txid>"
|
||||
EXPLORER_URL="https://chain.so/tx/ZECTEST/<txid>"
|
||||
|
||||
# Amount for staking a proposal in ZEC
|
||||
# Amount for staking a proposal in ZEC, keep in sync with backend .env
|
||||
PROPOSAL_STAKING_AMOUNT=0.025
|
||||
|
||||
# Normally production runs with SSL, this disables that
|
||||
|
@ -24,3 +24,6 @@ DISABLE_SSL=true
|
|||
|
||||
# Uncomment if running on testnet
|
||||
# TESTNET=true
|
||||
|
||||
# Maximum amount for a proposal target, keep in sync with backend .env
|
||||
PROPOSAL_TARGET_MAX=10000
|
||||
|
|
|
@ -36,6 +36,7 @@ class SignUp extends React.Component<Props> {
|
|||
name="name"
|
||||
placeholder="Non-unique name that others will see you as"
|
||||
autoComplete="name"
|
||||
maxLength={50}
|
||||
/>,
|
||||
)}
|
||||
</Form.Item>
|
||||
|
@ -47,6 +48,7 @@ class SignUp extends React.Component<Props> {
|
|||
<Input
|
||||
name="title"
|
||||
placeholder="A short description about you, e.g. Core Ethereum Developer"
|
||||
maxLength={50}
|
||||
/>,
|
||||
)}
|
||||
</Form.Item>
|
||||
|
@ -62,6 +64,7 @@ class SignUp extends React.Component<Props> {
|
|||
name="email"
|
||||
placeholder="We promise not to spam you or share your email"
|
||||
autoComplete="username"
|
||||
maxLength={255}
|
||||
/>,
|
||||
)}
|
||||
</Form.Item>
|
||||
|
|
|
@ -147,6 +147,7 @@ class CreateFlowBasics extends React.Component<Props, State> {
|
|||
type="text"
|
||||
value={title}
|
||||
onChange={this.handleInputChange}
|
||||
maxLength={200}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
|
@ -161,6 +162,7 @@ class CreateFlowBasics extends React.Component<Props, State> {
|
|||
value={brief}
|
||||
onChange={this.handleInputChange}
|
||||
rows={3}
|
||||
maxLength={200}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
|
@ -196,6 +198,7 @@ class CreateFlowBasics extends React.Component<Props, State> {
|
|||
value={target}
|
||||
onChange={this.handleInputChange}
|
||||
addonAfter="ZEC"
|
||||
maxLength={16}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import React from 'react';
|
||||
import { Form } from 'antd';
|
||||
import { Form, Alert } from 'antd';
|
||||
import MarkdownEditor from 'components/MarkdownEditor';
|
||||
import { ProposalDraft } from 'types';
|
||||
import { getCreateErrors } from 'modules/create/utils';
|
||||
|
||||
interface State {
|
||||
content: string;
|
||||
|
@ -22,6 +23,8 @@ export default class CreateFlowTeam extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const errors = getCreateErrors(this.state, true);
|
||||
|
||||
return (
|
||||
<Form layout="vertical" style={{ maxWidth: 980, margin: '0 auto' }}>
|
||||
<MarkdownEditor
|
||||
|
@ -29,6 +32,7 @@ export default class CreateFlowTeam extends React.Component<Props, State> {
|
|||
initialMarkdown={this.state.content}
|
||||
minHeight={200}
|
||||
/>
|
||||
{errors.content && <Alert type="error" message={errors.content} showIcon />}
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -124,6 +124,7 @@ const MilestoneFields = ({
|
|||
name="title"
|
||||
value={milestone.title}
|
||||
onChange={ev => onChange(index, { ...milestone, title: ev.currentTarget.value })}
|
||||
maxLength={80}
|
||||
/>
|
||||
<button
|
||||
onClick={() => onRemove(index)}
|
||||
|
@ -147,6 +148,7 @@ const MilestoneFields = ({
|
|||
onChange={ev =>
|
||||
onChange(index, { ...milestone, content: ev.currentTarget.value })
|
||||
}
|
||||
maxLength={255}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -190,6 +192,7 @@ const MilestoneFields = ({
|
|||
}
|
||||
addonAfter="%"
|
||||
style={{ maxWidth: '120px', width: '100%' }}
|
||||
maxLength={6}
|
||||
/>
|
||||
{index === 0 && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', marginLeft: '0.5rem' }}>
|
||||
|
|
|
@ -138,6 +138,10 @@
|
|||
font-size: 0.8rem;
|
||||
opacity: 0.3;
|
||||
animation: draft-notification-popup 120ms ease 1;
|
||||
|
||||
&.is-error {
|
||||
color: @error-color;
|
||||
}
|
||||
}
|
||||
|
||||
&-loading {
|
||||
|
|
|
@ -103,6 +103,7 @@ interface StateProps {
|
|||
form: AppState['create']['form'];
|
||||
isSavingDraft: AppState['create']['isSavingDraft'];
|
||||
hasSavedDraft: AppState['create']['hasSavedDraft'];
|
||||
saveDraftError: AppState['create']['saveDraftError'];
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
|
@ -149,7 +150,7 @@ class CreateFlow extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { isSavingDraft } = this.props;
|
||||
const { isSavingDraft, saveDraftError } = this.props;
|
||||
const { step, isPreviewing, isSubmitting, isShowingSubmitWarning } = this.state;
|
||||
|
||||
const info = STEP_INFO[step];
|
||||
|
@ -238,8 +239,16 @@ class CreateFlow extends React.Component<Props, State> {
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
{isSavingDraft && (
|
||||
{isSavingDraft ? (
|
||||
<div className="CreateFlow-draftNotification">Saving draft...</div>
|
||||
) : (
|
||||
saveDraftError && (
|
||||
<div className="CreateFlow-draftNotification is-error">
|
||||
Failed to save draft!
|
||||
<br />
|
||||
{saveDraftError}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
<SubmitWarningModal
|
||||
proposal={this.props.form}
|
||||
|
@ -326,13 +335,12 @@ class CreateFlow extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
const withConnect = connect<StateProps, DispatchProps, {}, AppState>(
|
||||
(state: AppState) => {
|
||||
return {
|
||||
form: state.create.form,
|
||||
isSavingDraft: state.create.isSavingDraft,
|
||||
hasSavedDraft: state.create.hasSavedDraft,
|
||||
};
|
||||
},
|
||||
(state: AppState) => ({
|
||||
form: state.create.form,
|
||||
isSavingDraft: state.create.isSavingDraft,
|
||||
hasSavedDraft: state.create.hasSavedDraft,
|
||||
saveDraftError: state.create.saveDraftError,
|
||||
}),
|
||||
{
|
||||
updateForm: createActions.updateForm,
|
||||
},
|
||||
|
|
|
@ -13,8 +13,6 @@ import {
|
|||
PROPOSAL_DETAIL_INITIAL_STATE,
|
||||
} from 'modules/proposals/reducers';
|
||||
|
||||
export const TARGET_ZEC_LIMIT = 1000;
|
||||
|
||||
interface CreateFormErrors {
|
||||
rfpOptIn?: string;
|
||||
title?: string;
|
||||
|
@ -57,7 +55,17 @@ export function getCreateErrors(
|
|||
skipRequired?: boolean,
|
||||
): CreateFormErrors {
|
||||
const errors: CreateFormErrors = {};
|
||||
const { title, team, milestones, target, payoutAddress, rfp, rfpOptIn, brief } = form;
|
||||
const {
|
||||
title,
|
||||
content,
|
||||
team,
|
||||
milestones,
|
||||
target,
|
||||
payoutAddress,
|
||||
rfp,
|
||||
rfpOptIn,
|
||||
brief,
|
||||
} = form;
|
||||
|
||||
// Required fields with no extra validation
|
||||
if (!skipRequired) {
|
||||
|
@ -90,10 +98,16 @@ export function getCreateErrors(
|
|||
errors.brief = 'Brief can only be 140 characters maximum';
|
||||
}
|
||||
|
||||
// Content limit for our database's sake
|
||||
if (content && content.length > 250000) {
|
||||
errors.content = 'Details can only be 250,000 characters maximum';
|
||||
}
|
||||
|
||||
// Amount to raise
|
||||
const targetFloat = target ? parseFloat(target) : 0;
|
||||
if (target && !Number.isNaN(targetFloat)) {
|
||||
const targetErr = getAmountError(targetFloat, TARGET_ZEC_LIMIT);
|
||||
const limit = parseFloat(process.env.PROPOSAL_TARGET_MAX as string);
|
||||
const targetErr = getAmountError(targetFloat, limit);
|
||||
if (targetErr) {
|
||||
errors.target = targetErr;
|
||||
}
|
||||
|
@ -134,6 +148,12 @@ export function getCreateErrors(
|
|||
return 'Payout percent is required';
|
||||
} else if (Number.isNaN(parseInt(ms.payoutPercent, 10))) {
|
||||
return 'Payout percent must be a valid number';
|
||||
} else if (parseInt(ms.payoutPercent, 10) !== parseFloat(ms.payoutPercent)) {
|
||||
return 'Payout percent must be a whole number, no decimals';
|
||||
} else if (parseInt(ms.payoutPercent, 10) <= 0) {
|
||||
return 'Payout percent must be greater than 0%';
|
||||
} else if (parseInt(ms.payoutPercent, 10) > 100) {
|
||||
return 'Payout percent must be less than or equal to 100%';
|
||||
}
|
||||
|
||||
// Last one shows percentage errors
|
||||
|
@ -155,10 +175,10 @@ export function getCreateErrors(
|
|||
}
|
||||
|
||||
export function validateUserProfile(user: User) {
|
||||
if (user.displayName.length > 30) {
|
||||
return 'Display name can only be 30 characters maximum';
|
||||
} else if (user.title.length > 30) {
|
||||
return 'Title can only be 30 characters maximum';
|
||||
if (user.displayName.length > 50) {
|
||||
return 'Display name can only be 50 characters maximum';
|
||||
} else if (user.title.length > 50) {
|
||||
return 'Title can only be 50 characters maximum';
|
||||
}
|
||||
|
||||
return '';
|
||||
|
|
|
@ -57,6 +57,7 @@ module.exports = () => {
|
|||
NODE_ENV: process.env.NODE_ENV || 'development',
|
||||
PORT: process.env.PORT || 3000,
|
||||
PROPOSAL_STAKING_AMOUNT: process.env.PROPOSAL_STAKING_AMOUNT,
|
||||
PROPOSAL_TARGET_MAX: process.env.PROPOSAL_TARGET_MAX || '10000',
|
||||
PUBLIC_HOST_URL: process.env.PUBLIC_HOST_URL,
|
||||
SENTRY_DSN: process.env.SENTRY_DSN || null,
|
||||
SENTRY_RELEASE: process.env.SENTRY_RELEASE || undefined,
|
||||
|
|
Loading…
Reference in New Issue