Skip to content

Commit ce9420a

Browse files
authored
Merge pull request #119 from gigalixir/feat-signup-2.0
feat: new signup support
2 parents abb3531 + 9a5d432 commit ce9420a

File tree

5 files changed

+306
-103
lines changed

5 files changed

+306
-103
lines changed

DEPLOYING.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ rm dist/*
1414
python3 -m build
1515
```
1616

17+
## Install locally
18+
19+
```
20+
pip install dist/gigalixir-X.Y.Z.tar.gz
21+
```
22+
23+
1724
## Upload and tsting from pypitest
1825

1926
```

gigalixir/__init__.py

Lines changed: 35 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,27 @@
55
from .openers.linux import LinuxOpener
66
from .openers.darwin import DarwinOpener
77
from .openers.windows import WindowsOpener
8-
from . import observer as gigalixir_observer
9-
from . import user as gigalixir_user
10-
from . import mfa as gigalixir_mfa
8+
from . import api_key as gigalixir_api_key
9+
from . import api_session as gigalixir_api_session
10+
from . import app_activity as gigalixir_app_activity
1111
from . import app as gigalixir_app
12+
from . import canary as gigalixir_canary
1213
from . import config as gigalixir_config
14+
from . import database as gigalixir_database
15+
from . import domain as gigalixir_domain
16+
from . import free_database as gigalixir_free_database
17+
from . import invoice as gigalixir_invoice
18+
from . import log_drain as gigalixir_log_drain
19+
from . import mfa as gigalixir_mfa
20+
from . import observer as gigalixir_observer
21+
from . import payment_method as gigalixir_payment_method
1322
from . import permission as gigalixir_permission
1423
from . import release as gigalixir_release
15-
from . import app_activity as gigalixir_app_activity
16-
from . import api_key as gigalixir_api_key
24+
from . import signup as gigalixir_signup
1725
from . import ssh_key as gigalixir_ssh_key
18-
from . import log_drain as gigalixir_log_drain
19-
from . import payment_method as gigalixir_payment_method
20-
from . import domain as gigalixir_domain
21-
from . import invoice as gigalixir_invoice
2226
from . import usage as gigalixir_usage
23-
from . import database as gigalixir_database
24-
from . import free_database as gigalixir_free_database
25-
from . import canary as gigalixir_canary
27+
from . import user as gigalixir_user
2628
from . import git
27-
from . import api_session as gigalixir_api_session
2829
import click
2930
import getpass
3031
import stripe
@@ -560,17 +561,29 @@ def logout(ctx):
560561
gigalixir_user.logout(ctx.obj['env'])
561562

562563
@cli.command()
563-
@click.option('-e', '--email', prompt=True)
564-
@click.option('-p', '--password', prompt=True, hide_input=True, confirmation_prompt=False)
564+
@click.option('-e', '--email', prompt=False)
565+
@click.option('-p', '--password', prompt=False)
565566
@click.option('-t', '--mfa_token', prompt=False) # we handle prompting if needed, not always needed
566567
@click.option('-y', '--yes', is_flag=True)
567568
@click.pass_context
568569
@report_errors
569-
def login(ctx, email, password, yes, mfa_token):
570+
def login(ctx, email, password, mfa_token, yes):
570571
"""
571572
Login and receive an api key.
572573
"""
573-
gigalixir_user.login(ctx.obj['session'], email, password, yes, ctx.obj['env'], mfa_token)
574+
method = None
575+
if not email and not password:
576+
print("How would you like to log in?")
577+
print("1. Email and password")
578+
print("2. Google authentication")
579+
while True:
580+
value = click.prompt('Authentication', type=int)
581+
if value == 1:
582+
return gigalixir_user.login(ctx.obj['session'], email, password, ctx.obj['env'], mfa_token)
583+
elif value == 2:
584+
return gigalixir_user.oauth_login(ctx.obj['session'], ctx.obj['env'], 'google')
585+
586+
gigalixir_user.login(ctx.obj['session'], email, password, ctx.obj['env'], mfa_token)
574587

575588
@cli.command(name='login:google')
576589
@click.option('-y', '--yes', is_flag=True)
@@ -580,7 +593,7 @@ def google_login(ctx, yes):
580593
"""
581594
Login with Google and receive an api key.
582595
"""
583-
gigalixir_user.oauth_login(ctx.obj['session'], yes, ctx.obj['env'], 'google')
596+
gigalixir_user.oauth_login(ctx.obj['session'], ctx.obj['env'], 'google')
584597

585598
# @get.command()
586599
@cli.command()
@@ -1063,46 +1076,20 @@ def current_running_usage(ctx):
10631076

10641077
# @create.command()
10651078
@cli.command()
1066-
@click.option('--email')
1067-
@click.option('-p', '--password')
1068-
@click.option('-y', '--accept_terms_of_service_and_privacy_policy', is_flag=True)
10691079
@click.pass_context
10701080
@report_errors
1071-
def signup(ctx, email, password, accept_terms_of_service_and_privacy_policy):
1081+
def signup(ctx):
10721082
"""
10731083
Sign up for a new account.
10741084
"""
1075-
if not accept_terms_of_service_and_privacy_policy:
1076-
logging.getLogger("gigalixir-cli").info("GIGALIXIR Terms of Service: https://www.gigalixir.com/terms")
1077-
logging.getLogger("gigalixir-cli").info("GIGALIXIR Privacy Policy: https://www.gigalixir.com/privacy")
1078-
if not click.confirm('Do you accept the Terms of Service and Privacy Policy?'):
1079-
raise Exception("You must accept the Terms of Service and Privacy Policy to continue.")
1080-
1081-
if email == None:
1082-
email = click.prompt('Email')
1083-
gigalixir_user.validate_email(ctx.obj['session'], email)
1085+
gigalixir_signup.by_email(ctx)
10841086

1085-
if password == None:
1086-
password = click.prompt('Password', hide_input=True)
1087-
gigalixir_user.validate_password(ctx.obj['session'], password)
1088-
1089-
gigalixir_user.create(ctx.obj['session'], email, password, accept_terms_of_service_and_privacy_policy)
10901087

10911088
@cli.command('signup:google')
1092-
@click.option('-y', '--accept_terms_of_service_and_privacy_policy', is_flag=True)
10931089
@click.pass_context
10941090
@report_errors
1095-
def google_signup(ctx, accept_terms_of_service_and_privacy_policy):
1096-
"""
1097-
Sign up for a new account using your google login.
1098-
"""
1099-
if not accept_terms_of_service_and_privacy_policy:
1100-
logging.getLogger("gigalixir-cli").info("GIGALIXIR Terms of Service: https://www.gigalixir.com/terms")
1101-
logging.getLogger("gigalixir-cli").info("GIGALIXIR Privacy Policy: https://www.gigalixir.com/privacy")
1102-
if not click.confirm('Do you accept the Terms of Service and Privacy Policy?'):
1103-
raise Exception("You must accept the Terms of Service and Privacy Policy to continue.")
1104-
1105-
gigalixir_user.oauth_create(ctx.obj['session'], ctx.obj['env'], "google")
1091+
def google_signup(ctx):
1092+
gigalixir_signup.by_oauth(ctx, "google")
11061093

11071094
@cli.command(name='ps:observer')
11081095
@click.option('-a', '--app_name', envvar="GIGALIXIR_APP")

gigalixir/signup.py

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
import click
2+
import datetime
3+
import re
4+
import stripe
5+
from . import netrc
6+
from . import user as gigalixir_user
7+
8+
def by_email(ctx):
9+
env = ctx.obj['env']
10+
session = ctx.obj['session']
11+
12+
print("Welcome, let's get you started!")
13+
print("")
14+
print("We require a *valid* email address to be on file in case we need to reach you about your applications.")
15+
print("We promise to not abuse your email address.")
16+
print("Don't believe us? Read our privacy policy: https://gigalixir.com/privacy-policy")
17+
print("")
18+
email = click.prompt('Email')
19+
uuid = set_email(session, email)
20+
21+
print("")
22+
print("Please check your email for the confirmation code we have sent you.")
23+
accepted = False
24+
while not accepted:
25+
code = click.prompt('Confirmation code')
26+
accepted = confirm(session, uuid, code)
27+
print("Thank you for your trust in us.")
28+
print("")
29+
30+
print("Now we need a password.")
31+
accepted = False
32+
while not accepted:
33+
password = click.prompt('Password', hide_input=True)
34+
accepted = set_password(session, uuid, password)
35+
print("")
36+
37+
(promo, tier) = select_tier(session, uuid)
38+
39+
confirm_and_complete(session, uuid, email, tier, promo, env)
40+
41+
def by_oauth(ctx, provider):
42+
env = ctx.obj['env']
43+
session = ctx.obj['session']
44+
45+
welcome_message()
46+
47+
(oauth_session, url, uuid) = start_oauth(session, provider)
48+
49+
email = gigalixir_user.oauth_process(session, provider, 'signup', url, oauth_session)['email']
50+
print("Thank you for your trust in us.")
51+
print("")
52+
53+
(promo, tier) = select_tier(session, uuid)
54+
55+
confirm_and_complete(session, uuid, email, tier, promo, env)
56+
57+
58+
def welcome_message():
59+
print("Welcome, let's get you started!")
60+
print("")
61+
62+
print("We require a *valid* email address to be on file in case we need to reach you about your applications.")
63+
print("We promise to not abuse your email address.")
64+
print("Don't believe us? Read our privacy policy: https://gigalixir.com/privacy-policy")
65+
print("")
66+
67+
def start_oauth(session, provider):
68+
r = session.post('/api/signup', json = { 'oauth': provider })
69+
if r.status_code != 200:
70+
raise Exception(r.text)
71+
72+
data = r.json()['data']
73+
return (data['session'], data['url'], data['uuid'])
74+
75+
def set_email(session, email):
76+
r = session.post('/api/signup', json = { 'email': email })
77+
if r.status_code == 429:
78+
raise Exception('Too many attempts. Please try again later.')
79+
if r.status_code != 200:
80+
raise Exception(r.text)
81+
82+
return r.json()['data']['uuid']
83+
84+
def confirm(session, uuid, code):
85+
r = session.post('/api/signup', json = { 'confirmation_code': code, 'uuid': uuid })
86+
if r.status_code == 429:
87+
raise Exception('Too many attempts. Please try again later.')
88+
89+
return r.status_code == 200
90+
91+
def set_password(session, uuid, password):
92+
r = session.post('/api/signup', json = { 'password': password, 'uuid': uuid })
93+
if r.status_code != 200:
94+
print(r.text)
95+
return False
96+
97+
return True
98+
99+
def set_cc(session, uuid, stripe_token):
100+
r = session.post('/api/signup', json = { 'stripe_token': stripe_token, 'uuid': uuid })
101+
if r.status_code == 429:
102+
raise Exception('Too many attempts. Please try again later.')
103+
104+
if r.status_code != 200:
105+
print(r.text)
106+
return False
107+
108+
return True
109+
110+
def finalize(session, uuid, tier):
111+
r = session.post('/api/signup', json = { 'tier': tier, 'uuid': uuid })
112+
if r.status_code != 200:
113+
raise Exception(r.text)
114+
115+
data = r.json()['data']
116+
return (data['email'], data['key'])
117+
118+
119+
## input validations
120+
def luhn_check(card_number):
121+
"""Validate credit card number using Luhn algorithm."""
122+
digits = [int(d) for d in card_number]
123+
checksum = 0
124+
125+
# Double every second digit from the right, subtracting 9 if >9
126+
for i, digit in enumerate(reversed(digits)):
127+
if i % 2 == 1:
128+
digit *= 2
129+
if digit > 9:
130+
digit -= 9
131+
checksum += digit
132+
133+
return checksum % 10 == 0
134+
135+
def validate_credit_card_number(card_number):
136+
"""Ensure card number is valid using regex and Luhn check."""
137+
card_number = card_number.replace(" ", "") # Allow spaces for readability
138+
if not re.fullmatch(r"\d{13,19}", card_number):
139+
raise click.BadParameter("Credit card number must be 13-19 digits long.")
140+
if not luhn_check(card_number):
141+
raise click.BadParameter("Invalid credit card number (failed Luhn check).")
142+
return card_number
143+
144+
def validate_cvv(cvv):
145+
"""Ensure CVV is numeric and correct length."""
146+
if not re.fullmatch(r"\d{3,4}", cvv):
147+
raise click.BadParameter("CVV must be 3 or 4 digits.")
148+
return cvv
149+
150+
def validate_exp_month(exp_month):
151+
"""Ensure expiration month is valid (1-12)."""
152+
try:
153+
month = int(exp_month)
154+
if 1 <= month <= 12:
155+
return f"{month:02d}" # Ensure two-digit format
156+
except ValueError:
157+
pass
158+
raise click.BadParameter("Expiration month must be a number between 1 and 12.")
159+
160+
def validate_exp_year(exp_year):
161+
"""Ensure expiration year is in the future and valid."""
162+
current_year = datetime.datetime.now().year
163+
try:
164+
year = int(exp_year)
165+
if current_year <= year <= current_year + 20: # Prevent unrealistic years
166+
return str(year)
167+
except ValueError:
168+
pass
169+
raise click.BadParameter(f"Expiration year must be {current_year} or later.")
170+
171+
def select_tier(session, uuid):
172+
promo = add_promo_code(session, uuid)
173+
print("")
174+
175+
tier = None
176+
if promo:
177+
tier = "STANDARD"
178+
else:
179+
tier = prompt_tier()
180+
print("")
181+
182+
if tier == "STANDARD":
183+
accepted = False
184+
while not accepted:
185+
card_number = click.prompt("Enter credit card number", type=str, value_proc=validate_credit_card_number)
186+
cvc = click.prompt("Enter CVV", type=str, value_proc=validate_cvv)
187+
exp_month = click.prompt("Enter expiration month (MM)", type=str, value_proc=validate_exp_month)
188+
exp_year = click.prompt("Enter expiration year (YYYY)", type=str, value_proc=validate_exp_year)
189+
190+
stripe_token = stripe.Token.create(card={ "number": card_number, "exp_month": exp_month, "exp_year": exp_year, "cvc": cvc })
191+
192+
accepted = set_cc(session, uuid, stripe_token["id"])
193+
print("")
194+
195+
return (promo, tier)
196+
197+
def add_promo_code(session, uuid):
198+
while True:
199+
promo = click.prompt('Promo code [enter to skip]', default="", show_default=False, type=str)
200+
if promo == "":
201+
return None
202+
else:
203+
r = session.post('/api/signup', json = { 'promo_code': promo, 'uuid': uuid })
204+
205+
if r.status_code == 200:
206+
return r.json()['data']['promo']
207+
208+
elif r.status_code == 429:
209+
raise Exception('Too many attempts. Please try again later.')
210+
211+
print("Invalid promo code")
212+
213+
def prompt_tier():
214+
print("Which tier would you like to sign up for?")
215+
print("1. Free")
216+
print("2. Standard")
217+
while True:
218+
value = click.prompt('Tier', type=int)
219+
if value == 1:
220+
return "FREE"
221+
elif value == 2:
222+
return "STANDARD"
223+
224+
def confirm_and_complete(session, uuid, email, tier, promo, env):
225+
print("")
226+
print("You are about to signup for the %s tier." % tier)
227+
print(" Email: %s" % email)
228+
229+
if promo:
230+
print(" Promo: %s" % promo)
231+
print("")
232+
233+
print("By continuing, you agree to our Terms of Service and Privacy Policy.")
234+
print(" Privacy Policy: https://gigalixir.com/privacy-policy")
235+
print(" Terms of Service: https://gigalixir.com/terms-of-service")
236+
print("")
237+
if not click.confirm('Do you wish to proceed?', default=True):
238+
raise Exception("Signup cancelled.")
239+
(email, key) = finalize(session, uuid, tier)
240+
241+
print("Welcome to Gigalixir!")
242+
netrc.update_netrc(email, key, env)

0 commit comments

Comments
 (0)