Beispiel #1
0
    def prepare(self, data):
        if self.request['session']['role'] != 'admin':
            if data.get('external_ticket_url'):
                raise JsonErrors.HTTPForbidden(
                    message='external_ticket_url may only be set by admins')
            elif data.get('external_donation_url'):
                raise JsonErrors.HTTPForbidden(
                    message='external_donation_url may only be set by admins')

        timezone: TzInfo = data.pop('timezone', None)
        if timezone:
            data['timezone'] = str(timezone)
        date = data.pop('date', None)
        if date:
            dt, duration = prepare_event_start(date['dt'], date['dur'],
                                               timezone)
            data.update(
                start_ts=dt,
                duration=duration,
            )

        loc = data.pop('location', None)
        if loc:
            data.update(
                location_name=loc['name'],
                location_lat=loc['lat'],
                location_lng=loc['lng'],
            )

        return data
Beispiel #2
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 #3
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 #4
0
 async def check_permissions(self):
     await check_session(self.request, 'admin', 'host')
     await _check_event_permissions(self.request, check_upcoming=True)
     user_status = await self.conn.fetchrow(
         'SELECT status FROM users WHERE id=$1', self.session['user_id'])
     if self.session['role'] != 'admin' and user_status != 'active':
         raise JsonErrors.HTTPForbidden(message='Host not active')
Beispiel #5
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 #6
0
async def guest_signin(request):
    model, siw_method = SIGNIN_MODELS[request.match_info['site']]
    m = await parse_request(request, model)
    details = await siw_method(m, app=request.app)

    company_id = request['company_id']
    user_id, status = await request['conn'].fetchrow_b(
        CREATE_USER_SQL,
        values=Values(
            company=company_id,
            role='guest',
            email=details['email'].lower(),
            first_name=details.get('first_name'),
            last_name=details.get('last_name'),
        ))
    if status == 'suspended':
        raise JsonErrors.HTTPBadRequest(message='user suspended')

    session = await new_session(request)
    session.update({
        'user_id': user_id,
        'user_role': 'guest',
        'last_active': int(time())
    })

    await record_action(request, user_id, ActionTypes.guest_signin)

    json_str = await request['conn'].fetchval(GET_USER_SQL, company_id,
                                              user_id)
    return raw_json_response(json_str)
Beispiel #7
0
    def prepare(self, data):
        if data.get('external_ticket_url') and self.request['session']['role'] != 'admin':
            raise JsonErrors.HTTPForbidden(message='external_ticket_url may only be set by admins')

        date = data.pop('date', None)
        timezone: TzInfo = data.pop('timezone', None)
        if timezone:
            data['timezone'] = str(timezone)
        if date:
            dt: datetime = timezone.localize(date['dt'].replace(tzinfo=None))
            duration: Optional[int] = date['dur']
            if duration:
                duration = timedelta(seconds=duration)
            else:
                dt = datetime(dt.year, dt.month, dt.day)
            data.update(
                start_ts=dt,
                duration=duration,
            )

        loc = data.pop('location', None)
        if loc:
            data.update(
                location_name=loc['name'],
                location_lat=loc['lat'],
                location_lng=loc['lng'],
            )

        return data
Beispiel #8
0
 async def prepare_add_data(self, data):
     role_type = data.pop('role_type')
     if role_type not in {UserRoles.host, UserRoles.admin}:
         raise JsonErrors.HTTPBadRequest(
             message='role must be either "host" or "admin".')
     data.update(role=role_type, company=self.request['company_id'])
     return data
Beispiel #9
0
async def donation_image_upload(request):
    co_id = request['company_id']
    don_opt_id = int(request.match_info['pk'])
    r = await request['conn'].fetchrow(
        """
        SELECT co.slug, cat.slug, d.image
        FROM donation_options AS d
        JOIN categories AS cat ON d.category = cat.id
        JOIN companies AS co ON cat.company = co.id
        WHERE d.id = $1 AND cat.company = $2
        """,
        don_opt_id,
        co_id,
    )
    if not r:
        raise JsonErrors.HTTPNotFound(message='donation option not found')

    co_slug, cat_slug, old_image = r
    content = await request_image(request, expected_size=IMAGE_SIZE)

    upload_path = Path(co_slug) / cat_slug / str(don_opt_id)
    image_url = await upload_other(
        content, upload_path=upload_path, settings=request.app['settings'], req_size=IMAGE_SIZE, thumb=True,
    )

    await request['conn'].execute('UPDATE donation_options SET image=$1 WHERE id=$2', image_url, don_opt_id)

    if old_image:
        await delete_image(old_image, request.app['settings'])
    return json_response(status='success')
Beispiel #10
0
async def user_tickets(request):
    user_id = int(request.match_info['pk'])
    if request['session']['role'] != 'admin' and user_id != request['session']['user_id']:
        raise JsonErrors.HTTPForbidden(message='wrong user')

    json_str = await request['conn'].fetchval(user_tickets_sql, user_id)
    return raw_json_response(json_str)
Beispiel #11
0
async def donation_after_prepare(request):
    donation_option_id = int(request.match_info['don_opt_id'])
    event_id = int(request.match_info['event_id'])
    conn = request['conn']
    r = await conn.fetchrow(
        """
        SELECT opt.name, opt.amount, cat.id
        FROM donation_options AS opt
        JOIN categories AS cat ON opt.category = cat.id
        WHERE opt.id = $1 AND opt.live AND cat.company = $2
        """,
        donation_option_id,
        request['company_id'],
    )
    if not r:
        raise JsonErrors.HTTPBadRequest(message='donation option not found')

    name, amount, cat_id = r
    event = await conn.fetchval(
        'SELECT 1 FROM events WHERE id=$1 AND category=$2', event_id, cat_id)
    if not event:
        raise JsonErrors.HTTPBadRequest(
            message='event not found on the same category as donation_option')

    user_id = request['session']['user_id']
    action_id = await record_action_id(request,
                                       user_id,
                                       ActionTypes.donate_prepare,
                                       event_id=event_id,
                                       donation_option_id=donation_option_id)

    client_secret = await stripe_payment_intent(
        user_id=user_id,
        price_cents=int(amount * 100),
        description=f'donation to {name} ({donation_option_id}) after booking',
        metadata={
            'purpose': 'donate',
            'event_id': event_id,
            'reserve_action_id': action_id,
            'user_id': user_id
        },
        company_id=request['company_id'],
        idempotency_key=f'idempotency-donate-{action_id}',
        app=request.app,
        conn=conn,
    )
    return json_response(client_secret=client_secret, action_id=action_id)
Beispiel #12
0
    async def execute(self, m: Model):
        event_id = await _check_event_permissions(self.request)
        ticket_id = int(self.request.match_info['tid'])
        r = await self.conn.fetchrow(
            """
            select a.type, t.price, a.extra->>'charge_id'
            from tickets as t
            join actions as a on t.booked_action = a.id
            where t.event = $1 and t.id = $2 and t.status = 'booked'
            """,
            event_id,
            ticket_id,
        )
        if not r:
            raise JsonErrors.HTTPNotFound(message='Ticket not found')
        booking_type, price, charge_id = r
        if m.refund_amount is not None:
            if booking_type != ActionTypes.buy_tickets:
                raise JsonErrors.HTTPBadRequest(
                    message=
                    'Refund not possible unless ticket was bought through stripe.'
                )
            if m.refund_amount > price:
                raise JsonErrors.HTTPBadRequest(
                    message=f'Refund amount must not exceed {price:0.2f}.')

        async with self.conn.transaction():
            action_id = await record_action_id(
                self.request, self.session['user_id'],
                ActionTypes.cancel_booked_tickets)
            await self.conn.execute(
                "update tickets set status='cancelled', cancel_action=$1 where id=$2",
                action_id, ticket_id)
            await self.conn.execute('SELECT check_tickets_remaining($1, $2)',
                                    event_id, self.settings.ticket_ttl)
            if m.refund_amount is not None:
                await stripe_refund(
                    refund_charge_id=charge_id,
                    ticket_id=ticket_id,
                    amount=int(m.refund_amount * 100),
                    user_id=self.session['user_id'],
                    company_id=self.request['company_id'],
                    app=self.app,
                    conn=self.conn,
                )
        await self.app['email_actor'].send_tickets_available(event_id)
Beispiel #13
0
 async def execute(self, m: Model):
     action_id = int(self.request.match_info['action_id'])
     v = await self.conn.execute(
         'update actions set extra=extra || $3 where id=$1 and user_id=$2',
         action_id, self.session['user_id'], json.dumps({'gift_aid': m.dict()})
     )
     if v != 'UPDATE 1':
         raise JsonErrors.HTTPNotFound(message='action not found')
Beispiel #14
0
async def category_public(request):
    conn: BuildPgConnection = request['conn']
    company_id = request['company_id']
    category_slug = request.match_info['category']
    json_str = await conn.fetchval(CATEGORY_PUBLIC_SQL, company_id, category_slug)
    if not json_str:
        raise JsonErrors.HTTPNotFound(message='category not found')
    return raw_json_response(json_str)
Beispiel #15
0
async def _check_event_permissions(request, check_upcoming=False):
    event_id = int(request.match_info['id'])
    r = await request['conn'].fetchrow(
        """
        SELECT host, start_ts
        FROM events AS e
        JOIN categories AS cat ON e.category = cat.id
        WHERE e.id=$1 AND cat.company=$2
        """, event_id, request['company_id'])
    if not r:
        raise JsonErrors.HTTPNotFound(message='event not found')
    host_id, start_ts = r
    if request['session']['role'] != 'admin':
        if host_id != request['session']['user_id']:
            raise JsonErrors.HTTPForbidden(message='user is not the host of this event')
        if check_upcoming and start_ts < datetime.utcnow().replace(tzinfo=timezone.utc):
            raise JsonErrors.HTTPForbidden(message="you can't modify past events")
    return event_id
Beispiel #16
0
async def _get_cat_img_path(request):
    cat_id = int(request.match_info['cat_id'])
    conn: BuildPgConnection = request['conn']
    try:
        co_slug, cat_slug = await conn.fetchrow(CAT_IMAGE_SQL, request['company_id'], cat_id)
    except TypeError:
        raise JsonErrors.HTTPNotFound(message='category not found')
    else:
        return Path(co_slug) / cat_slug / 'option'
Beispiel #17
0
async def event_tickets(request):
    event_id = int(request.match_info['id'])
    if request['session']['user_role'] == 'host':
        host_id = await request['conn'].fetchval('SELECT host FROM events WHERE id=$1', event_id)
        if host_id != request['session']['user_id']:
            raise JsonErrors.HTTPForbidden(message='use is not the host of this event')

    json_str = await request['conn'].fetchval(event_ticket_sql, event_id, request['company_id'])
    return raw_json_response(json_str)
Beispiel #18
0
 async def check_item_permissions(self, pk):
     v = await self.conn.fetchval_b(
         ':query',
         query=await self.check_item_permissions_query(pk),
         print_=self.print_queries,
     )
     if not v:
         raise JsonErrors.HTTPNotFound(
             message=f'{self.meta["single_title"]} not found')
Beispiel #19
0
async def event_public(request):
    conn: BuildPgConnection = request['conn']
    company_id = request['company_id']
    category_slug = request.match_info['category']
    event_slug = request.match_info['event']
    json_str = await conn.fetchval(event_sql, company_id, category_slug, event_slug)
    if not json_str:
        raise JsonErrors.HTTPNotFound(message='event not found')
    return raw_json_response(json_str)
Beispiel #20
0
async def category_default_image(request):
    m = await parse_request(request, ImageActionModel)

    path = await _get_cat_img_path(request)
    images = await list_images(path, request.app['settings'])
    if m.image not in images:
        raise JsonErrors.HTTPBadRequest(message='image does not exist')
    cat_id = int(request.match_info['cat_id'])
    await request['conn'].execute('UPDATE categories SET image = $1 WHERE id = $2', m.image, cat_id)
    return json_response(status='success')
Beispiel #21
0
    async def execute(self, m: Model):
        event_id = await _check_event_permissions(self.request, check_upcoming=True)
        existing = [tt for tt in m.ticket_types if tt.id]
        deleted_with_tickets = await self.conn.fetchval(
            """
            SELECT 1
            FROM ticket_types AS tt
            JOIN tickets AS t ON tt.id = t.ticket_type
            WHERE tt.event=$1 AND NOT (tt.id=ANY($2))
            GROUP BY tt.id
            """, event_id, [tt.id for tt in existing]
        )
        if deleted_with_tickets:
            raise JsonErrors.HTTPBadRequest(message='ticket types deleted which have ticket associated with them')

        async with self.conn.transaction():
            await self.conn.fetchval(
                """
                DELETE FROM ticket_types
                WHERE ticket_types.event=$1 AND NOT (ticket_types.id=ANY($2))
                """, event_id, [tt.id for tt in existing]
            )

            for tt in existing:
                v = await self.conn.execute_b(
                    'UPDATE ticket_types SET :values WHERE id=:id AND event=:event',
                    values=SetValues(**tt.dict(exclude={'id'})),
                    id=tt.id,
                    event=event_id,
                )
                if v != 'UPDATE 1':
                    raise JsonErrors.HTTPBadRequest(message='wrong ticket updated')

            new = [tt for tt in m.ticket_types if not tt.id]
            if new:
                await self.conn.execute_b(
                    """
                    INSERT INTO ticket_types (:values__names) VALUES :values
                    """,
                    values=MultipleValues(*(Values(event=event_id, **tt.dict(exclude={'id'})) for tt in new))
                )
            await record_action(self.request, self.request['session']['user_id'], ActionTypes.edit_event,
                                event_id=event_id, subtype='edit-ticket-types')
Beispiel #22
0
 def conflict_exc(self, exc: UniqueViolationError):
     columns = re.search(r'\((.+?)\)',
                         exc.as_dict()['detail']).group(1).split(', ')
     return JsonErrors.HTTPConflict(
         message='Conflict',
         details=[{
             'loc': [col],
             'msg':
             f'This value conflicts with an existing "{col}", try something else.',
             'type': 'value_error.conflict',
         } for col in columns if col in self.model.__fields__])
Beispiel #23
0
 def where_pk(self, pk) -> Where:
     if pk < 1:
         raise JsonErrors.HTTPBadRequest(
             message='request pk must be greater than 0')
     where = self.where()
     is_pk = self.pk_ref() == pk
     if where:
         where.logic = where.logic & is_pk
     else:
         where = Where(is_pk)
     return where
Beispiel #24
0
async def set_event_image_existing(request):
    m = await parse_request(request, ImageModel)
    if not m.image.startswith(request.app['settings'].s3_domain):
        raise JsonErrors.HTTPBadRequest(message='image not allowed')

    await _delete_existing_image(request)

    event_id = int(request.match_info['id'])
    await request['conn'].execute('UPDATE events SET image=$1 WHERE id=$2', m.image, event_id)
    await record_action(request, request['session']['user_id'], ActionTypes.edit_event,
                        event_id=event_id, subtype='set-image-existing')
    return json_response(status='success')
Beispiel #25
0
async def clear_email_def(request):
    trigger = get_trigger(request)
    r = await request['conn'].execute(
        'DELETE FROM email_definitions WHERE trigger=$1 AND company=$2',
        trigger,
        request['company_id'],
    )
    if r == 'DELETE 1':
        return json_response(status='ok')
    else:
        raise JsonErrors.HTTPNotFound(
            message=f'email definition with trigger "{trigger}" not found')
Beispiel #26
0
async def switch_user_status(request):
    user_id = int(request.match_info['pk'])
    status = await request['conn'].fetchval(
        'SELECT status FROM users WHERE id=$1 AND company=$2',
        user_id,
        request['company_id'],
    )
    if not status:
        raise JsonErrors.HTTPNotFound(message='user not found')
    new_status = 'suspended' if status == 'active' else 'active'
    await request['conn'].execute('UPDATE users SET status=$1 WHERE id=$2', new_status, user_id)
    return json_response(new_status=new_status)
Beispiel #27
0
 async def check_permissions(self):
     await check_session(self.request, 'admin')
     v = await self.conn.fetchval_b(
         """
         SELECT 1 FROM events AS e
         JOIN categories AS c on e.category = c.id
         WHERE e.id=:id AND c.company=:company
         """,
         id=int(self.request.match_info['id']),
         company=self.request['company_id']
     )
     if not v:
         raise JsonErrors.HTTPNotFound(message='Event not found')
Beispiel #28
0
async def category_delete_image(request):
    m = await parse_request(request, ImageModel)
    await _check_image_exists(request, m)
    cat_id = int(request.match_info['cat_id'])

    dft_image = await request['conn'].fetchval(
        'SELECT image FROM categories WHERE id=$1', cat_id)
    if dft_image == m.image:
        raise JsonErrors.HTTPBadRequest(
            message='default image may not be be deleted')

    await delete_image(m.image, request.app['settings'])
    return json_response(status='success')
Beispiel #29
0
async def check_event_sig(request):
    company_id = request['company_id']
    category_slug = request.match_info['category']
    event_slug = request.match_info['event']
    r = await request['conn'].fetchrow(event_id_public_sql, company_id,
                                       category_slug, event_slug)

    # so we always do the hashing even for an event that does exist to avoid timing attack, probably over kill
    if r:
        event_id, event_is_public = r
    else:
        event_id, event_is_public = 0, False

    if not event_is_public:
        url_sig = request.match_info.get('sig')
        if not url_sig:
            raise JsonErrors.HTTPNotFound(message='event not found')
        sig = hmac.new(request.app['settings'].auth_key.encode(),
                       f'/{category_slug}/{event_slug}/'.encode(),
                       digestmod=hashlib.md5).hexdigest()
        if not compare_digest(url_sig, sig):
            raise JsonErrors.HTTPNotFound(message='event not found')
    return event_id
Beispiel #30
0
    async def edit(self, pk) -> web.Response:
        await self.check_item_permissions(pk)
        m = await parse_request_ignore_missing(self.request, self.model)

        data = await self.prepare_edit_data(pk, m.dict(exclude_unset=True))
        if not data:
            raise JsonErrors.HTTPBadRequest(message=f'no data to save')

        try:
            await self.edit_execute(pk, **data)
        except UniqueViolationError as e:
            raise self.conflict_exc(e)
        else:
            return json_response(status='ok')