|  |  | @ -1,10 +1,14 @@ | 
			
		
	
		
		
			
				
					
					|  |  |  | import datetime as dt |  |  |  | import datetime as dt | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | import re | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | from apistar import http, Route, Include |  |  |  | from apistar.exceptions import HTTPException | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | from apistar import http, Route, Include, App | 
			
		
	
		
		
			
				
					
					|  |  |  | from apistar_jwt.token import JWT, JWTUser |  |  |  | from apistar_jwt.token import JWT, JWTUser | 
			
		
	
		
		
			
				
					
					|  |  |  | # from apistar_mail import Message, Mail |  |  |  | from apistar_mail import Message, Mail | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  | # from sqlalchemy.exc import IntegrityError, InvalidRequestError |  |  |  | # from sqlalchemy.exc import IntegrityError, InvalidRequestError | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | from itsdangerous import URLSafeTimedSerializer, BadSignature | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | import logbook | 
			
		
	
		
		
			
				
					
					|  |  |  | from sqlalchemy.orm import Session |  |  |  | from sqlalchemy.orm import Session | 
			
		
	
		
		
			
				
					
					|  |  |  | from sqlalchemy.orm.exc import NoResultFound |  |  |  | from sqlalchemy.orm.exc import NoResultFound | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
	
		
		
			
				
					|  |  | @ -12,6 +16,31 @@ from cookie_api.models import User | 
			
		
	
		
		
			
				
					
					|  |  |  | from cookie_api.schema import UserExportSchema, UserCreateSchema |  |  |  | from cookie_api.schema import UserExportSchema, UserCreateSchema | 
			
		
	
		
		
			
				
					
					|  |  |  | from cookie_api.util import ExtJSONResponse |  |  |  | from cookie_api.util import ExtJSONResponse | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | # https://realpython.com/handling-email-confirmation-in-flask/ | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | EMAIL_REGEX = r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | email_regex = re.compile(EMAIL_REGEX) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | logger = logbook.Logger(__name__) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | def generate_confirmation_token(email): | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     serializer = URLSafeTimedSerializer('secret-key') | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     return serializer.dumps(email, salt='password-salt') | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | def confirm_token(token, expiration=3600): | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     serializer = URLSafeTimedSerializer('secret-key') | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     try: | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         email = serializer.loads( | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |             token, | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |             salt='password-salt', | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |             max_age=expiration | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         ) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     except BadSignature: | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         return False | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     return email | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | def login(json_data: http.RequestData, session: Session, jwt: JWT): |  |  |  | def login(json_data: http.RequestData, session: Session, jwt: JWT): | 
			
		
	
		
		
			
				
					
					|  |  |  |     user_id = json_data.get('email') |  |  |  |     user_id = json_data.get('email') | 
			
		
	
	
		
		
			
				
					|  |  | @ -53,33 +82,61 @@ def logout(): | 
			
		
	
		
		
			
				
					
					|  |  |  |     pass |  |  |  |     pass | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | # TODO Add email confirmation to registration |  |  |  | def register(user_data: UserCreateSchema, session: Session, mail: Mail, app: App) -> UserExportSchema: | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  | # def register(user_data: UserCreateSchema, session: Session, mail: Mail): |  |  |  |     if not email_regex.match(user_data['email']): | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  | def register(user_data: UserCreateSchema, session: Session) -> UserExportSchema: |  |  |  |         raise HTTPException('Please provide a valid email address', status_code=400) | 
			
				
				
			
		
	
		
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     email_check = session.query(User).filter_by(email=user_data['email']).one_or_none() |  |  |  |     email_check = session.query(User).filter_by(email=user_data['email']).one_or_none() | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     if email_check is not None: |  |  |  |     if email_check is not None: | 
			
		
	
		
		
			
				
					
					|  |  |  |         error = { |  |  |  |         raise HTTPException('user email address is already in use', status_code=400) | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |             'status': 'error', |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             'message': 'user email address is already in use' |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         } |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         return ExtJSONResponse(error, 400) |  |  |  |  | 
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     user = User(email=user_data['email'], password=user_data['password']) |  |  |  |     user = User(email=user_data['email'], password=user_data['password']) | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     try: | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         tokenized_email = generate_confirmation_token(user.email) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         email_body = app.render_template('confirm_email.jinja2', | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |                                          confirm_url=f'http://localhost:5000/auth/confirm?token={tokenized_email}' | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |                                          ) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         msg = Message(subject="Confirm your email", | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |                       html=email_body, | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |                       recipients=[user_data['email']]) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         mail.send(msg) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     except Exception as e: | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         logger.error(e, exc_info=True) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         raise HTTPException('Error an sending email', status_code=500) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     session.add(user) |  |  |  |     session.add(user) | 
			
		
	
		
		
			
				
					
					|  |  |  |     session.commit() |  |  |  |     session.commit() | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     # msg = Message("Thank you for registering please confirm your email", recipients=[user_rep['email']]) |  |  |  |     headers = {} | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |     # mail.send(msg) |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     # headers = {} |  |  |  |  | 
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |     message = { |  |  |  |     message = { | 
			
		
	
		
		
			
				
					
					|  |  |  |         'status': 'success', |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         'message': 'Please check your inbox and confirm your email', |  |  |  |         'message': 'Please check your inbox and confirm your email', | 
			
		
	
		
		
			
				
					
					|  |  |  |         'data': UserExportSchema(user) |  |  |  |         'data': UserExportSchema(user) | 
			
		
	
		
		
			
				
					
					|  |  |  |     } |  |  |  |     } | 
			
		
	
		
		
			
				
					
					|  |  |  |     return ExtJSONResponse(message, 201) |  |  |  |     return ExtJSONResponse(message, 201, headers) | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | # TODO Return an HTML response since the user will be clicking on a link in a web browser | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | # TODO Since you are returning a user respresentation add a location and Content-Location header | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | def confirm(params: http.QueryParams, session: Session): | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     token = params['token'] | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     email = confirm_token(token) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     if not email: | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         raise HTTPException('Email token is expired or invalid', status_code=400) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     user = session.query(User).filter_by(email=email).one() | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     if user.confirmed: | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         raise HTTPException('Account already confirmed. Please login.', status_code=400) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     user.confirmed = True | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     session.add(user) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     session.commit() | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     return ExtJSONResponse(UserExportSchema(user)) | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | def user_profile(user: JWTUser, session: Session) -> UserExportSchema: |  |  |  | def user_profile(user: JWTUser, session: Session) -> UserExportSchema: | 
			
		
	
	
		
		
			
				
					|  |  | @ -87,15 +144,10 @@ def user_profile(user: JWTUser, session: Session) -> UserExportSchema: | 
			
		
	
		
		
			
				
					
					|  |  |  |         user = session.query(User).filter_by(id=user.id).one() |  |  |  |         user = session.query(User).filter_by(id=user.id).one() | 
			
		
	
		
		
			
				
					
					|  |  |  |     except NoResultFound as e: |  |  |  |     except NoResultFound as e: | 
			
		
	
		
		
			
				
					
					|  |  |  |         error = {'message': str(e)} |  |  |  |         error = {'message': str(e)} | 
			
		
	
		
		
			
				
					
					|  |  |  |         return ExtJSONResponse(error, 400) |  |  |  |         raise HTTPException(error, status_code=400) | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |     return ExtJSONResponse(UserExportSchema(user)) |  |  |  |     return ExtJSONResponse(UserExportSchema(user)) | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | # TODO Add email confirmation |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | def confirm(json_data: http.RequestData, session: Session): |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     pass |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | # TODO Add email password reset |  |  |  | # TODO Add email password reset | 
			
		
	
		
		
			
				
					
					|  |  |  | def reset(): |  |  |  | def reset(): | 
			
		
	
		
		
			
				
					
					|  |  |  |     pass |  |  |  |     pass | 
			
		
	
	
		
		
			
				
					|  |  | @ -104,7 +156,8 @@ def reset(): | 
			
		
	
		
		
			
				
					
					|  |  |  | routes = [ |  |  |  | routes = [ | 
			
		
	
		
		
			
				
					
					|  |  |  |     Route('/login', 'POST', login), |  |  |  |     Route('/login', 'POST', login), | 
			
		
	
		
		
			
				
					
					|  |  |  |     Route('/register', 'POST', register), |  |  |  |     Route('/register', 'POST', register), | 
			
		
	
		
		
			
				
					
					|  |  |  |     Route('/status', 'GET', user_profile) |  |  |  |     Route('/status', 'GET', user_profile), | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     Route('/confirm', 'GET', confirm) | 
			
		
	
		
		
			
				
					
					|  |  |  | ] |  |  |  | ] | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | auth_routes = [Include('/auth', name='auth', routes=routes)] |  |  |  | auth_routes = [Include('/auth', name='dirp', routes=routes)] | 
			
				
				
			
		
	
		
		
	
	
		
		
			
				
					|  |  | 
 |