async def host_signup(request): signin_method = request.match_info['site'] model, siw_method = SIGNUP_MODELS[signin_method] m: GrecaptchaModel = await parse_request(request, model) await check_grecaptcha(m, get_ip(request), app=request.app) details = await siw_method(m, app=request.app) company_id = request['company_id'] conn = request['conn'] r = await conn.fetchrow( 'SELECT role, status FROM users WHERE email=$1 AND company=$2', details['email'], company_id) existing_role = None if r: existing_role, status = r if existing_role != 'guest': raise JsonErrors.HTTP470(status='existing-user') if status == 'suspended': raise JsonErrors.HTTP470(message='user suspended') user_id = await request['conn'].fetchval_b( """ INSERT INTO users (:values__names) VALUES :values ON CONFLICT (company, email) DO UPDATE SET role=EXCLUDED.role RETURNING id """, values=Values( company=company_id, role='host', status='active' if signin_method in {'facebook', 'google'} else 'pending', email=details['email'].lower(), first_name=details.get('first_name'), last_name=details.get('last_name'), )) session = await new_session(request) session.update({ 'user_id': user_id, 'user_role': 'host', 'last_active': int(time()) }) await record_action(request, user_id, ActionTypes.host_signup, existing_user=bool(existing_role), signin_method=signin_method) await request.app['email_actor'].send_account_created(user_id) json_str = await request['conn'].fetchval(GET_USER_SQL, company_id, user_id) return raw_json_response(json_str)
async def set_password(request): conn = request['conn'] m = await parse_request(request, PasswordModel, headers_=HEADER_CROSS_ORIGIN) user_id = decrypt_json(request.app, m.token.encode(), ttl=3600 * 24 * 7, headers_=HEADER_CROSS_ORIGIN) nonce = m.token[:20] already_used = await conn.fetchval( """ SELECT 1 FROM actions WHERE user_id=$1 AND type='password-reset' AND now() - ts < interval '7 days' AND extra->>'nonce'=$2 """, user_id, nonce, ) if already_used: raise JsonErrors.HTTP470( message='This password reset link has already been used.', headers_=HEADER_CROSS_ORIGIN) user = await conn.fetchrow( 'SELECT id, first_name, last_name, email, role, status, company FROM users WHERE id=$1', user_id) user = dict(user) if user.pop('company') != request['company_id']: # should not happen raise JsonErrors.HTTPBadRequest( message='company and user do not match') if user['status'] == 'suspended': raise JsonErrors.HTTP470( message='user suspended, password update not allowed.', headers_=HEADER_CROSS_ORIGIN) pw_hash = mk_password(m.password1, request.app['settings']) del m await conn.execute( "UPDATE users SET password_hash=$1, status='active' WHERE id=$2", pw_hash, user_id) await record_action(request, user_id, ActionTypes.password_reset, nonce=nonce) return successful_login(user, request.app, HEADER_CROSS_ORIGIN)
async def check_email(m: EmailModel, app): if await validate_email(m.email, app.loop): return m.dict() else: raise JsonErrors.HTTP470( status='invalid', message=f'"{m.email}" doesn\'t look like an active email address.')
async def set_password(request): h = {'Access-Control-Allow-Origin': 'null'} conn = request['conn'] m = await parse_request(request, PasswordModel, error_headers=h) data = decrypt_json(request.app, m.token, ttl=3600 * 24 * 7) user_id, nonce = data['user_id'], data['nonce'] already_used = await conn.fetchval( """ SELECT 1 FROM actions WHERE user_id=$1 AND type='password-reset' AND now() - ts < interval '7 days' AND extra->>'nonce'=$2 """, user_id, nonce) if already_used: raise JsonErrors.HTTP470( message='This password reset link has already been used.') company_id, status = await conn.fetchrow( 'SELECT company, status FROM users WHERE id=$1', user_id) if company_id != request['company_id']: # should not happen raise JsonErrors.HTTPBadRequest( message='company and user do not match') if status == 'suspended': raise JsonErrors.HTTP470( message='user suspended, password update not allowed.') pw_hash = mk_password(m.password1, request.app['settings']) del m await conn.execute( "UPDATE users SET password_hash=$1, status='active' WHERE id=$2", pw_hash, user_id) await record_action(request, user_id, ActionTypes.password_reset, nonce=nonce) return json_response(status='success', headers_=h)
async def execute(self, m: Model): # noqa: C901 (ignore complexity) event_id = int(self.request.match_info['id']) ticket_count = len(m.tickets) if ticket_count > self.settings.max_tickets: raise JsonErrors.HTTPBadRequest( message='Too many tickets reserved') user_id = self.session['user_id'] status, external_ticket_url, event_name, cover_costs_percentage = await self.conn.fetchrow( """ SELECT e.status, e.external_ticket_url, e.name, c.cover_costs_percentage FROM events AS e JOIN categories c on e.category = c.id WHERE c.company=$1 AND e.id=$2 """, self.request['company_id'], event_id) if status != 'published': raise JsonErrors.HTTPBadRequest(message='Event not published') if external_ticket_url is not None: raise JsonErrors.HTTPBadRequest( message='Cannot reserve ticket for an externally ticketed event' ) r = await self.conn.fetchrow( 'SELECT price FROM ticket_types WHERE event=$1 AND id=$2', event_id, m.ticket_type) if not r: raise JsonErrors.HTTPBadRequest(message='Ticket type not found') item_price, *_ = r if self.settings.ticket_reservation_precheck: # should only be false during CheckViolationError tests tickets_remaining = await self.conn.fetchval( 'SELECT check_tickets_remaining($1, $2)', event_id, self.settings.ticket_ttl) if tickets_remaining is not None and ticket_count > tickets_remaining: raise JsonErrors.HTTP470( message=f'only {tickets_remaining} tickets remaining', tickets_remaining=tickets_remaining) total_price, item_extra_donated = None, None if item_price: total_price = item_price * ticket_count if cover_costs_percentage and m.tickets[0].cover_costs: item_extra_donated = item_price * cover_costs_percentage / 100 total_price += item_extra_donated * ticket_count try: async with self.conn.transaction(): update_user_preferences = await self.create_users(m.tickets) action_id = await record_action_id(self.request, user_id, ActionTypes.reserve_tickets, event_id=event_id) ticket_values = [ Values( email=t.email and t.email.lower(), first_name=t.first_name, last_name=t.last_name, extra_info=t.extra_info or None, ) for t in m.tickets ] await self.conn.execute_b( """ WITH v (email, first_name, last_name, extra_info) AS (VALUES :values) INSERT INTO tickets (event, reserve_action, ticket_type, price, extra_donated, user_id, first_name, last_name, extra_info) SELECT :event, :reserve_action, :ticket_type, :price, :extra_donated, u.id, v.first_name, v.last_name, v.extra_info FROM v LEFT JOIN users AS u ON v.email=u.email AND u.company=:company_id """, event=event_id, reserve_action=action_id, ticket_type=m.ticket_type, price=item_price, extra_donated=item_extra_donated, company_id=self.request['company_id'], values=MultipleValues(*ticket_values), ) await self.conn.execute( 'SELECT check_tickets_remaining($1, $2)', event_id, self.settings.ticket_ttl) except CheckViolationError as exc: if exc.constraint_name != 'ticket_limit_check': # pragma: no branch raise # pragma: no cover logger.warning('CheckViolationError: %s', exc) raise JsonErrors.HTTPBadRequest( message='insufficient tickets remaining') res = Reservation( user_id=user_id, action_id=action_id, price_cent=total_price and int(total_price * 100), event_id=event_id, ticket_count=ticket_count, event_name=event_name, ) if total_price: client_secret = await stripe_buy_intent(res, self.request['company_id'], self.app, self.conn) else: client_secret = None if update_user_preferences: # has to happen after the transactions is finished await self.app['donorfy_actor'].update_user( self.request['session']['user_id'], update_user=False) return { 'booking_token': encrypt_json(self.app, res.dict()), 'action_id': action_id, 'ticket_count': ticket_count, 'item_price': item_price and float(item_price), 'extra_donated': item_extra_donated and float(item_extra_donated * ticket_count), 'total_price': total_price and float(total_price), 'timeout': int(time()) + self.settings.ticket_ttl - 30, 'client_secret': client_secret, }
async def execute(self, m: Model): event_id = int(self.request.match_info['id']) ticket_count = len(m.tickets) if ticket_count < 1: raise JsonErrors.HTTPBadRequest(message='at least one ticket must be purchased') status, event_price, event_name = await self.conn.fetchrow( """ SELECT e.status, e.price, e.name FROM events AS e JOIN categories c on e.category = c.id WHERE c.company=$1 AND e.id=$2 """, self.request['company_id'], event_id ) if status != 'published': raise JsonErrors.HTTPBadRequest(message='Event not published') tickets_remaining = await self.conn.fetchval( 'SELECT check_tickets_remaining($1, $2)', event_id, self.settings.ticket_ttl ) if tickets_remaining is not None and ticket_count > tickets_remaining: raise JsonErrors.HTTP470(message=f'only {tickets_remaining} tickets remaining', tickets_remaining=tickets_remaining) # TODO check user isn't already booked try: async with self.conn.transaction(): user_lookup = await self.create_users(m.tickets) action_id = await record_action_id(self.request, self.session['user_id'], ActionTypes.reserve_tickets) await self.conn.execute_b( 'INSERT INTO tickets (:values__names) VALUES :values', values=MultipleValues(*[ Values( event=event_id, user_id=user_lookup[t.email.lower()] if t.email else None, reserve_action=action_id, extra=to_json_if(t.dict(include={'dietary_req', 'extra_info'})), ) for t in m.tickets ]) ) await self.conn.execute('SELECT check_tickets_remaining($1, $2)', event_id, self.settings.ticket_ttl) except CheckViolationError as e: logger.warning('CheckViolationError: %s', e) raise JsonErrors.HTTPBadRequest(message='insufficient tickets remaining') user = await self.conn.fetchrow( """ SELECT id, full_name(first_name, last_name, email) AS name, email, role FROM users WHERE id=$1 """, self.session['user_id'] ) # TODO needs to work when the event is free price_cent = int(event_price * ticket_count * 100) res = Reservation( user_id=self.session['user_id'], action_id=action_id, price_cent=price_cent, event_id=event_id, ticket_count=ticket_count, event_name=event_name, ) return { 'booking_token': encrypt_json(self.app, res.dict()), 'ticket_count': ticket_count, 'item_price_cent': int(event_price * 100), 'total_price_cent': price_cent, 'user': dict(user), 'timeout': int(time()) + self.settings.ticket_ttl, }