Beispiel #1
0
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)
Beispiel #2
0
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)
Beispiel #3
0
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.')
Beispiel #4
0
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)
Beispiel #5
0
    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,
        }
Beispiel #6
0
    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,
        }