This repository has been archived by the owner on Nov 15, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathlib_auth.py
415 lines (319 loc) · 11.3 KB
/
lib_auth.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
"""
Functions related user logins, signups, password authentications,
logouts, etc ...
"""
from lib_db import User, UserID, Group, VerifyUser
from google.appengine.api import mail
import hashlib
import random
import re
import string
import stripe
PASS_RE = re.compile(r"^.{3,20}$")
EMAIL_RE = re.compile(r"^[\S]+@[\S]+\.[\S]+$")
# Define an exception for authentication errors
class AuthExcept(Exception):
msg = ''
def __init__(self, msg):
self.msg = msg
def get_cookie_string(email):
"""
Creates a cookie string to use for authenticating users.
user_id|encrypted_password
"""
user = User.all().filter("email =", email).fetch(1)[0]
name = 'user'
value = str(user.user_id) + '|' + str(user.password)
return '%s=%s; Path=/' % (name, value)
def make_salt():
"""
Create a random string to salt up the encryption
"""
return ''.join(
[random.choice(string.letters + string.digits)
for i in range(10)])
def encrypt_password(password, salt):
"""
Encrypts a password with sha256, but should be upgraded to bcrypt
once google has that python library in app engine.
"""
return hashlib.sha256(password + salt).hexdigest()
def make_userid():
"""
Generates the next user id number from the database.
"""
uid = UserID.all().fetch(1)
if not len(uid):
uid = UserID(next_id=1)
else:
uid = uid[0]
# update ids
current_id = uid.next_id
next_id = current_id + 1
uid.next_id = next_id
uid.put()
return current_id
def make_user(email, password, parent=None, user_id=None):
if User.all().ancestor(parent).filter("email =", email).fetch(1):
raise AuthExcept("email exists")
if user_id is None:
user_id = make_userid()
salt = make_salt()
encrypted_password = encrypt_password(password, salt)
admin_user = User(user_id=user_id,
parent=parent,
email=email,
password=encrypted_password,
salt=salt)
admin_user.put()
return admin_user
def signup(email, password, parent=None):
"""
Checks for valid inputs then adds a user to the User database.
"""
exists = User.all().ancestor(parent).filter("email =", email)
if (exists.fetch(1)):
raise AuthExcept("Account Exists")
if not EMAIL_RE.match(email):
raise AuthExcept("Invalid Email")
if not PASS_RE.match(password):
raise AuthExcept("Invalid Password")
salt = make_salt()
encrypted_password = encrypt_password(password, salt)
temp_id = hashlib.sha256(make_salt()).hexdigest()
# Set up groups. See if the email domain exists
groups = ['public']
domain = email.split('@')[1]
g = Group.all().ancestor(parent).filter("name =", domain).fetch(1)
if g:
groups.append(domain)
user = VerifyUser(email=email, password=encrypted_password,
salt=salt, temp_id=temp_id,
group=groups, parent=parent)
user.put()
print("http://modelr.io/verify_email?user_id=%s" %
str(user.temp_id))
mail.send_mail(sender="Hello <[email protected]>",
to="<%s>" % user.email,
subject="Modelr email verification",
body="""
Welcome to Modelr!
We need to verify your email address. Click the link below to validate your account and continue to billing.
http://modelr.io/verify_email?user_id=%s
Cheers,
Matt, Evan, and Ben
""" % str(user.temp_id))
return temp_id
def verify_signup(user_id, parent):
"""
Checks that a user id is in the queue to be added. The temporary
user id is sent through email verification. Raises a AuthExcept if
the id is invalid, otherwise returns the temporary user object
from the database.
:param user_id: User id from email verification
:param parent: Ancestor database of the temporary user
:returns the temporary user object.
"""
u = VerifyUser.all().ancestor(parent).filter("temp_id =", user_id)
verified_user = u.fetch(1)
# Check for success
if not verified_user:
raise AuthExcept("Verification Failed")
return verified_user[0]
def initialize_user(email, stripe_id, parent, tax_code, price, tax):
"""
Takes a verified user email from the authentication queue and adds
it to the permanent database with a stripe id.
:param verified_email: email of the verified user to add.
:param stripe_id: The stripe customer id of the user.
:param parent: The ancestor database key to use for the database.
:param tax_code: The tax code for the user
(province abbrieviation)
"""
verified_filter = VerifyUser.all()\
.ancestor(parent)\
.filter("email =", email)
verified_user = verified_filter.fetch(1)
if not verified_user:
raise AuthExcept("verification failed")
verified_user = verified_user[0]
# Make new user and populate
user = User(parent=parent)
user.user_id = make_userid()
user.email = verified_user.email
user.password = verified_user.password
user.salt = verified_user.salt
user.group = verified_user.group
user.stripe_id = stripe_id
user.tax_code = tax_code
for group in user.group:
g = Group.all().ancestor(parent).filter("name =",
group).fetch(1)
g[0].allowed_users.append(user.user_id)
g[0].put()
user.put()
# remove the temporary user from the queue
verified_user.delete()
# send a payment confirmation email
mail.send_mail(sender="Hello <[email protected]>",
to="<%s>" % user.email,
subject="Modelr subscription confirmation",
body="""
Welcome to Modelr!
You are now subscribed to Modelr! Your receipt is below.
To unsubscribe, please reply to this email or log in to Modelr and check your user settings.
Cheers,
Matt, Evan, and Ben
=======================
modelr.io
=======================
Monthly fee USD{0:.2f}
Sales tax USD{1:.2f}
Total USD{2:.2f}
========================
Modelr is a product of
Agile Geoscience Ltd
Nova Scotia - Canada
Canada Revenue Agency
reg # 840217913RT0001
========================
""".format(price/100., tax/100., (price+tax)/100.))
def signin(email, password, parent):
"""
Checks if a email and password are valid. Will throw a AuthExcept
if they are not.
"""
user = User.all().ancestor(parent).filter("email =",
email).fetch(1)
if not user:
raise AuthExcept('invalid email')
user = user[0]
encrypted_password = encrypt_password(password, user.salt)
if not encrypted_password == user.password:
raise AuthExcept('invalid password')
def verify(userid, password, ancestor):
"""
Verifies that the userid and encrypted password from a cookie
match the database
"""
try:
user = User.all().ancestor(ancestor)\
.filter("user_id =",
int(userid)).fetch(1)[0]
verified = (user.password == password)
return user
except IndexError:
verified = False
def authenticate(func):
"""
Wrapper function for methods that require a logged in
user
"""
def authenticate_and_call(self, *args, **kwargs):
user = self.verify()
if user is None:
self.redirect('/signup')
return
else:
return func(self, user, *args, **kwargs)
return authenticate_and_call
def send_message(subject, message):
"""
Sends us a message from a user or non-user.
"""
# send the message
mail.send_mail(sender="Hello <[email protected]>",
to="[email protected]",
subject=subject,
body=message)
def forgot_password(email, parent):
"""
Sets a new password after the user forgot it.
"""
user = User.all().ancestor(parent).filter("email =",
email).fetch(1)
if not user:
raise AuthExcept('invalid email')
user = user[0]
def generate_password(size=8,
chars=(string.ascii_uppercase +
string.digits)):
return ''.join(random.choice(chars) for x in range(size))
new = generate_password()
# send a new password email
mail.send_mail(sender="Hello <[email protected]>",
to="<%s>" % user.email,
subject="Modelr password reset",
body="""
Here's your new password!
%s
Please sign in with this new password, and then change it in your
profile page.
http://modelr.io/signin?redirect=settings
Cheers,
Matt, Evan, and Ben
""" % new
)
# Change it in the database
user.password = encrypt_password(new, user.salt)
user.put()
def reset_password(user, current_pword, new_password,
verify):
"""
Resets the password at the user's request.
:param user: The user database object requesting the password
change.
:param current_pword: The user's current password to verify.
:param new_password: The user's new password.
:param verify: The new password verification.
"""
# This check should be done in the javascript on the page
if new_password != verify:
raise AuthExcept("New password verification failed")
# Check if the original password matches the database
if encrypt_password(current_pword, user.salt) != user.password:
raise AuthExcept("Incorrect password")
# Update the password in the database
user.password = encrypt_password(new_password, user.salt)
# Save it in the database
user.put()
def cancel_subscription(user):
"""
Delete the user. See notes in DeleteHandler() in main.py
"""
try:
stripe_customer = stripe.Customer.retrieve(user.stripe_id)
# Check for extra invoices, ie Taxes, that also need
# to be cancelled.
invoice_items = stripe.InvoiceItem.all(customer=stripe_customer)
for invoice in invoice_items.data:
invoice_id = invoice["id"]
# get the invoice and delete it if we can
invoice_obj = stripe.InvoiceItem.retrieve(invoice_id)
try:
invoice_obj.delete()
except:
msg = """
invoice # {0} not deleted from stripe id {1}
""".format(invoice_id, user.stripe_id)
send_message("invoice not deleted",
msg)
sub_id = stripe_customer.subscriptions["data"][0]["id"]
stripe_customer.subscriptions\
.retrieve(sub_id).delete(at_period_end=True)
user.unsubscribed = True
user.put()
# TODO MailChimp
except Exception as e:
print e
raise AuthExcept("Failed to unsubscribe user: " + user.email)
mail.send_mail(sender="Hello <[email protected]>",
to="<%s>" % user.email,
subject="Modelr account deleted",
body="""
You have unsubscribed from Modelr. Your account will be deleted
at the end of the billing cycle.
Thank you for using Modelr. We hope to meet again some day.
Cheers,
Matt, Evan, and Ben
""")