Exemplo n.º 1
0
    def create_email(self, request):
        subject = request.json.get('subject')
        if not subject:
            return api.bad_request(form_errors=dict(subject=['标题不能为空']))

        content = request.json.get('content')
        if not content:
            return api.bad_request(form_errors=dict(content=['内容不能为空']))

        from_email = request.json.get('from_email')
        if not from_email:
            return api.bad_request(form_errors=dict(from_email=['发件人不能为空']))

        recipients = request.json.get('recipients')
        if not recipients:
            return api.bad_request(form_errors=dict(recipients=['收件人列表不能为空']))
        if not isinstance(recipients, list):
            return api.resopnse(400,
                                form_errors=dict(recipients=['收件人列表必须是数组']))

        email = send_email(recipients,
                           subject,
                           from_email=from_email,
                           content=content)

        return api.ok(data=email.serialize())
Exemplo n.º 2
0
    def create_model(self, request):
        create_args = super().create_model(request, no_save=True)

        project_id = request.json.get('project')
        if not project_id:
            return api.bad_request(message='missing project id')
        create_args['project_id'] = project_id

        pagetype = request.json.get('pagetype')
        Page = ProjectPage.PAGE_TYPES.get(pagetype)
        if not Page:
            return api.bad_request(message=f'invalid pagetype: {pagetype}')

        page = Page.objects.create(**create_args)

        if pagetype == 'gallery':
            self.set_gallery_images(page, request.json.get('images', []))

        if pagetype == 'homepage':
            self.set_homepage_carousel_items(
                page, request.json.get('carousel_items', []))

        self.set_attachments(page, request.json.get('attachments', []))

        return api.ok(data=page)
Exemplo n.º 3
0
def api_reset_password(request):
    """修改密码 API

    支持使用旧密码来设置新密码,也支持使用验证码来设置新密码。

    在实现时,会将所有参数传递给 authenticate(),如果成功,则设置新密码。

    POST /api/account/reset-password
    {
        // 与登录 API 中的登录凭证一致
        **credentials,

        // 设置的新密码
        new_password: '******',
    }
    """
    new_password = request.json.pop('new_password', None)
    if not new_password:
        return api.bad_request(message='Missing "new_password"')

    user = authenticate(request, **request.json, usage=otp_tools.USAGES.reset_password)
    if user:
        user.set_password(new_password)
        user.save()
        return api.ok(message='密码已更新')
    else:
        # TODO 应该给出更多的判断,从而给用户提供一些有效的报错信息
        # * 使用短信验证码重置密码时,如果手机号不存在,应该提示用户未注册
        #   注意,不应该在发送验证码时提示,而应该在发送之后再提示,避免信息泄漏。
        #   如果验证码错误应该提示验证码错误。
        # * 如果用户被锁定,可以提示用户被锁定
        # * 根据实际使用场景,提示旧密码错误/验证码错误等
        return api.bad_request(message='用户不存在或校验失败')
Exemplo n.º 4
0
    def create_sms(self, request):
        phone_numbers = request.json.get('phone_numbers')
        if not phone_numbers:
            return api.bad_request(form_errors=dict(
                phone_numbers=['接收方手机号不能为空']))

        signature_name = request.json.get('signature_name')
        if not signature_name:
            return api.bad_request(form_errors=dict(signature_name=['签名不能为空']))
        if signature_name not in AliSms.SIGNATURES:
            return api.bad_request(form_errors=dict(
                signature_name=['不允许使用的签名']))

        template_code = request.json.get('template_code')
        if not template_code:
            return api.bad_request(form_errors=dict(
                template_code=['模板编号不能为空']))
        if template_code not in AliSms.TEMPLATES:
            return api.bad_request(form_errors=dict(template_code=['模板编号不存在']))

        template_param = reuqest.json.get('template_param', {})

        sms = send_sms(phone_numbers, signature_name, template_code,
                       template_param)

        return api.ok(data=sms.serialize())
Exemplo n.º 5
0
def api_verify_code(request):
    """检查验证码是否正确

    POST /api/account/verify-code
    {
        // 验证码用途,可以是:register, login, reset-password
        "usage": "register",

        // 与 send-code 类似,有两种方式指定发送对象
        // * 提供 phone 或 email 字段
        // * 提供 identity 字段
        "phone": "13912345678",
        "email": "*****@*****.**",
        "identity": "13912345678",

        // 用户输入的验证码
        "code": "123456",
    }

    如果成功,返回 200,否则返回 400。
    """
    usage = request.json.get('usage')
    if usage not in otp_tools.USAGES:
        return api.bad_request(message='Invalid "usage"')

    code = request.json.get('code')

    identity = request.json.get('identity')
    phone = request.json.get('phone')
    email = request.json.get('email')

    if phone:
        if not is_valid_phone(phone):
            return errors.InvalidPhone()
    elif email:
        if not is_valid_email(email):
            return errors.InvalidEmail()
    elif identity:
        if is_valid_phone(identity):
            phone = identity
        elif is_valid_email(identity):
            email = identity
        else:
            return errors.InvalidIdentity()
    else:
        return api.bad_request(message='Missing "phone", "email" or "identity"')

    recipient = phone or email
    engine = 'sms' if phone else 'email'
    valid = otp_tools.verify_code(engine, request, recipient, usage, code, False)
    if valid:
        return api.ok(message='验证码正确')
    else:
        return errors.InvalidVerifyCode()
Exemplo n.º 6
0
def create_file(request, Model):
    bucket = request.POST.get('bucket')
    if not bucket:
        return api.bad_request(message=f'无效的 "bucket": {bucket}')

    if not request.FILES:
        return api.bad_request(message=f'没有上传文件')

    files = []
    for file in request.FILES.values():
        files.append(Model.objects.create(bucket, file, owner=request.user))

    return api.ok(data=[file.serialize(request=request) for file in files])
Exemplo n.º 7
0
    def patch_model(self, request, pk, no_save=False):
        """
        PATCH /adpi/admin/.../{model}/<pk>
        """
        try:
            model = self.get_queryset(request).get(pk=pk)
        except self.MODEL.DoesNotExist:
            return api.not_found()

        update_fields = []
        for panel in self.get_edit_panels(request):
            kwargs = panel.get_edit_kwargs(request)
            if kwargs:
                for key, value in kwargs.items():
                    setattr(model, key, value)
                    update_fields.append(key)

        if not update_fields:
            return api.bad_request(message='No field to update')

        if hasattr(model, 'updated_at'):
            update_fields.append('updated_at')

        if no_save:
            return model
        else:
            model.save()
            return api.ok(data=model.serialize(
                **self.get_serialize_kwargs(request)))
Exemplo n.º 8
0
def delete_file(request, Model, id):
    try:
        file = Model.objects.filter(deleted_at__isnull=True).get(id=id)
        file.delete()
        return api.ok(data=file)
    except Model.DoesNotExist:
        return api.not_found()
    except Exception as e:
        # NOTE 可能是由于其他资源依赖该文件,而相应资源上设置了 on_delete=models.PROTECT
        # 由于该请求仅在管理后台使用,所以可以直接把 exception 输出
        return api.bad_request(message=f'删除失败:{e}')
Exemplo n.º 9
0
def patch_file(request, Model, id):
    try:
        file = Model.objects.filter(deleted_at__isnull=True).get(id=id)
    except Model.DoesNotExist:
        return api.not_found()

    update_fields = []

    for field in ['filename', 'title']:
        if field in request.json:
            value = request.json[field]
            if not value:
                return api.bad_request(message=f'无效的 {field}: {value}')
            update_fields.append(field)
            setattr(file, field, value)

    file.save(update_fields=update_fields)

    return api.ok(data=file, message='文件修改成功')
Exemplo n.º 10
0
    def patch_model(self, request, pk):
        model = super().patch_model(request, pk, no_save=True)

        if 'project' in request.json:
            model.project_id = request.json['project']

        if 'pagetype' in request.json:
            pagetype = request.json['pagetype']
            if pagetype != model.PAGE_TYPE:
                return api.bad_request(message=f'不能修改页面类型')

        model.save()

        if model.PAGE_TYPE == 'gallery':
            self.set_gallery_images(model, request.json.get('images', []))

        if pagetype == 'homepage':
            self.set_homepage_carousel_items(
                model, request.json.get('carousel_items', []))

        self.set_attachments(model, request.json.get('attachments', []))

        return api.ok(data=model)
Exemplo n.º 11
0
def training(request):
    """
    知享呼吸培训认证相关页面(课程调研、培训课程、资格认证、考试评定)

    当使用 GET 请求时,渲染网页。用户在课程调研、资格认证、考试评定页面,
    可以点击「立即参加」按钮,此时需使用 API 的方式发送 POST 请求(注意,
    不是 form 的方式,必须是 API 的方式),POST 返回的 data 中有 redirect
    字段,该字段的值即为需要跳转的字段,前端可以直接跳转。

    POST /zhixiang/training/
    {
        action: 'start-a', // 取值:start-a, start-c, start-d
    }
    """
    status, _ = ZhixiangTraining.objects.select_related(
        'examination').get_or_create(user=request.user)

    # 首先处理 POST 请求。我们只考虑正常情况,其他情况均返回 400 bad request
    if request.method == 'POST':
        action = request.json.get('action')
        if not action or action not in ('start-a', 'start-c', 'start-d'):
            return api.bad_request(message='缺少 action 参数或无效的 action')

        if action == 'start-a' and status.a1:
            status.set_timestamp('a_start')
            return api.ok(data=dict(redirect=ZX_SETTINGS['training']
                                    ['investigation_wjx_url_pattern'] %
                                    request.user.id, ))
        elif action == 'start-c' and (status.c1 or status.c4):
            status.set_timestamp('c_start')
            return api.ok(data=dict(redirect=ZX_SETTINGS['training']
                                    ['qualification_wjx_url_pattern'] %
                                    request.user.id, ))
        elif action == 'start-d' and status.b2 and status.d1:
            status.set_timestamp('d_start')
            return api.ok(data=dict(redirect=status.examination.wjx_url, ))

        # 所有其他情况,返回 400
        # 在前端展示合理的情况下,用户正常操作不应该触发其他情况
        return api.bad_request()

    # 接下来处理 GET 请求,我们需要准备每一个 tab 需要展示的文案信息
    """
    给前端提供的 context:
    {
        status: obj, // 这是 ZhixiangTraining 对象,正常情况下前端应该不需要使用

        default_tab: 'a', // 取值 a, b, c, d,表示默认显示哪一个 tab

        a: {
            status: 1, // 有两套文案,1 表示提示用户参与调研,2 为用户已参与调研后提示的内容
            // 两种状态的处理
            // 1. 用户未参加调研,显示介绍文案,以及「立即参加」按钮
            // 2. 用户已参加调研,文案提示已参与调研,无参加按钮
        },

        b: {
            courses: [course], // 课程列表
            // 其中,course 有这些有用的字段:
            // course.thumbnail.url: 课程缩略图
            // course.introduction: 课程介绍
            // course.lesson_url: 点击课程后需要跳转的地址
            status: 1, // 取值:1,2
            // 两种状态
            // 1. 用户点击课程时,提示请先参与课程调研
            // 2. 用户点击后进入课程页面
        },

        c: {
            status: 1, // 取值 1,2,3,4
            // 四种状态的处理:
            // 1. 未参加资格认证,显示介绍文案,以及「立即参加」按钮
            // 2. 已填表、待审核,文案提示已填表、等待审核,无参加按钮
            // 3. 已通过审核,文案提示已已审核通过,无参加按钮
            // 4. 审核被驳回,文案提示审核被驳回,可以重新填表,以及「立即参加」按钮
        },

        d: {
            status: 1, // 取值 1,2,3,4
            // 四种状态的处理:
            // 1. 未通过资质认证,文案提示请先通过资质认证,无参加按钮
            // 2. 已通过资质认证,未学习完课程,文案提示请先完成 xx 课程学习,无参加按钮
            // 3. 可以参加考试,显示「立即参加」按钮
            // 4. 已参与考试,文案提示等待官方公布结果
            course: obj, // 如果用户已经通过了资格认证,course 的值是其需要完成的课程,可以用于渲染文案
        },
    }
    """

    # (a1, *, *, *): 默认显示课程调研页
    # (a2, b2, c3, *): 默认显示考试页
    if status.a1:
        default_tab = 'a'
    elif status.a2 and status.b2 and status.c3:
        default_tab = 'd'
    else:
        default_tab = 'b'
    # 如果有 show 参数,则使用 show 参数
    show = request.GET.get('show')
    if show in ['a', 'b', 'c', 'd']:
        default_tab = show

    # 课程调研页状态(前端状态码正好等于后端 a 的值 )
    # (a1, *, *, *): 显示「立即参加」按钮
    # (a2, *, *, *): 文案提示已参与调研,无按钮
    a_status = status.a

    # 培训课程页状态(前端状态码正好等于后端 a 的值)
    # (a1, *, *, *): 显示课程列表,点击课程时,弹窗提示:“参与调研后,才可参加培训!”
    # (a2, *, *, *): 显示课程列表
    b_status = status.a

    # 资格认证页状态(前端状态码正好等于后端 c 的值)
    # (*, *, C1, *): 显示「立即参加」按钮
    # (*, *, C2, *): 文案提示已填表,等待审核,不显示「立即参加」按钮
    # (*, *, C3, *): 文案提示已通过,不显示「立即参加」按钮
    # (*, *, C4, *): 文案提示审核被驳回,可以重新填表,显示「立即参加」按钮
    c_status = status.c

    # 考试评定页
    # (*, *, C1|C2|C4, *): 文案提示请先通过资格审核
    # (*, B1, C3, D1): 文案提示需要学习完某一个课程才能参加考试,请先学习
    # (*, B2, C3, D1): 可以参加考试,显示「立即参加」按钮
    # (*, *, *, D2):文案提示已参加考试,等待官方公布考试结果,不显示「立即参加」按钮
    if status.c1 or status.c2 or status.c4:
        d_status = 1
    elif status.b1 and status.c3 and status.d1:
        d_status = 2
    elif status.b2 and status.c3 and status.d1:
        d_status = 3
    elif status.d2:
        d_status = 4

    exam_course = status.examination.course if status.examination else None

    courses = Course.objects \
            .filter(zhixiang_exams__isnull=False) \
            .select_related('thumbnail') \
            .distinct()
    courses = [course for course in courses if course.published]
    for course in courses:
        course.fetch_presentationlesson_details(request.user)
        course.lesson_url = reverse(
            'zhixiang-lesson',
            kwargs=dict(id=course.default_presentationlesson.id))

    context = {
        'default_tab': default_tab,
        'a': {
            'status': a_status,
        },
        'b': {
            'status': b_status,
            'courses': courses
        },
        'c': {
            'status': c_status,
        },
        'd': {
            'status': d_status,
            'course': exam_course
        },
    }
    return render_for_ua(request,
                         'cardpc/zhixiang/training.html',
                         context=context)
Exemplo n.º 12
0
def api_login(request):
    """
    登录,网站前端、管理后台均使用该 API 登录。

    POST /api/account/login
    {
        // 登录凭证,直接传递给各个 Auth 后端,如何使用由 Auth 后端决定
        **credentials,

        // 可选参数,如果提供了,则只有指定角色的用户可以登录
        // 一般网站登录时无需此参数,管理后台登录时,需 role = "admin"
        "role": "superuser",
    }

    项目中启用了哪几个 Auth 后端,请在 settings.py 中找 AUTHENTICATION_BACKENDS,
    如果 AUTHENTICATION_BACKENDS 中没有配置的话,默认只有一个,即 ModelBackend。

    以下是几个 Backend 接受的 **credentails 参数:

    ModelBackend:
    * username(注意,cardpc 中通过 username 查找用户时,会搜索 username、phone、email)
    * password

    n.d.account.backends.SmsCodeBackend:
    * phone
    * code
    * identity: 如果没有提供 phone 参数,但是提供了 identity,则用 identity 作为 phone

    n.d.account.backends.EmailCodeBackend:
    * email
    * code
    * identity: 如果没有提供 email 参数,但是提供了 identity,则用 identity 作为 email

    简单的说,前端使用密码登录时,可以发送这样的请求:

    POST /api/account/login
    {
        username: xxx,
        password: xxx
    }

    使用邮箱或验证码登录时,可以发送这样的请求:

    POST /api/account/login
    {
        identity: xxx, // xxx 可以是邮箱或手机号,会自动检测
        code: xxx
    }

    如果登录时仅希望短信验证码、不希望尝试邮箱验证码,可以这样:

    POST /api/account/login
    {
        phone: xxx,
        code: xxx
    }
    """
    role = request.json.pop('role', None)

    user = authenticate(request, **request.json)
    if user is None:
        return api.bad_request(message='用户名或密码不正确')

    if role and role not in get_user_roles(user):
        return api.bad_request(message='用户名或密码不正确')

    login(request, user)
    return api.ok(message='登录成功', data=serialize_user(user))
Exemplo n.º 13
0
def api_send_code(request):
    """发送验证码,此函数仅支持发送注册、短信登录、重置密码三种验证码

    POST /api/account/send-code
    {
        // 验证码用途,可以是:register, login, reset-password
        "usage": "register",

        // 有两种方式指定发送对象,
        // * 提供 phone 字段,则会发送短信, 提供 email 字段,则会发送邮件
        // * 提供 identity 字段,则会自动判断 identity 是手机号还是邮箱,从而执行相应的操作
        // phone、email、identity 三个参数只能提供一个
        "phone": "13912345678",
        "email": "*****@*****.**",
        "identity": "13912345678",
    }

    * 如果缺少参数(一般只在开发阶段),会返回 400,message 描述错误信息。
    * 如果传递的 phone、email 或 identity 非法,会返回 InvalidPhone, InvalidEmail 或 InvalidIdentity
    * 如果在静默期内重复请求,会返回 RateExceeded
    * 其他所有情况都会返回 200(比如被后端认定为恶意请求而没有发送,比如登录请求中提供的
      username 不存在而未发送等等),这些错误无需告知用户。

    登录、重置密码验证码,后端会查找用户,如果用户不存在,则不会发送验证码。
    如果用户存在,但是该用户的 phone_validated 为 False(即手机号未验证),
    也不会发送验证码。邮箱也是如此。即发送验证码的前置条件是相应的短信或邮箱已经验证过。

    对于注册验证码,我们不检查账号是否已经存在,也不会因为账号已存在而不发送短信。原因:
    * 如果我们告诉用户账号已存在,那么有心人可以遍历手机号来判断我们有哪些注册账号
    * 如果我们不告诉用户账号已存在、也不发送验证码,那么用户会很奇怪(用户收不到验证码,
      会觉得很奇怪)。让用户正常收验证码,在注册时会提示账号已存在。
    """
    usage = request.json.get('usage')
    if usage not in otp_tools.USAGES:
        return api.bad_request(message='Invalid "usage"')

    identity = request.json.get('identity')
    phone = request.json.get('phone')
    email = request.json.get('email')

    if phone:
        if not is_valid_phone(phone):
            return errors.InvalidPhone()
    elif email:
        if not is_valid_email(email):
            return errors.InvalidEmail()
    elif identity:
        if is_valid_phone(identity):
            phone = identity
        elif is_valid_email(identity):
            email = identity
        else:
            return errors.InvalidIdentity()
    else:
        return api.bad_request(message='Missing "phone", "email" or "identity"')

    # 对于登录、重置密码的验证码,我们需要校验用户存在
    if usage in ['login', 'reset-password']:
        if phone:
            user = User.objects.filter(phone=phone, phone_validated=True).first()
        else:
            user = User.objects.filter(email=email, email_validated=True).first()

        if not user or not user.is_active:
            # 用户不存在或用户已锁定,不发送验证码,但仍然返回“验证码已发送”
            return api.ok(message='验证码已发送')

    recipient = phone or email
    engine = 'sms' if phone else 'email'
    vcode, result = otp_tools.generate_code(engine, request, recipient, usage=usage)
    if result == otp_tools.GENERATE_RESULTS.silent:
        return errors.RateExceeded()

    return api.ok(message='验证码已发送')
Exemplo n.º 14
0
def api_register(request):
    """
    注册 API

    POST /api/account/register
    {
        // 与登录 API 类似,可以明确指定 phone+code 或 email+code 或 identity+code
        "identity": "13912345678",
        "code": "123456",

        "phone": "13912345678",
        "code": "123456",

        "email": "*****@*****.**",
        "code": "123456",

        "password": "******",
    }
    """
    code = request.json.get('code')
    if not code:
        return errors.InvalidVerifyCode()

    password = request.json.get('password')
    if not password:
        return api.bad_request('Missing "password"')

    identity = request.json.get('identity')
    phone = request.json.get('phone')
    email = request.json.get('email')

    if phone:
        if not is_valid_phone(phone):
            return errors.InvalidPhone()
    elif email:
        if not is_valid_email(email):
            return errors.InvalidEmail()
    elif identity:
        if is_valid_phone(identity):
            phone = identity
        elif is_valid_email(identity):
            email = identity
        else:
            return errors.InvalidIdentity()
    else:
        return api.bad_request(
            message='Missing "phone", "email" or "identity"')

    if phone:
        find_user_args = dict(phone=phone, phone_validated=True)
        create_user_args = dict(phone=phone,
                                phone_validated=True,
                                password=password)
        vcode_engine = 'sms'
        recipient = phone
    else:
        create_user_args = dict(email=email,
                                email_validated=True,
                                password=password)
        vcode_engine = 'email'
        recipient = email

    valid = otp_tools.verify_code(vcode_engine, request, recipient, 'register',
                                  code, True)
    if not valid:
        return errors.InvalidVerifyCode()

    # 由于数据库中不宜为 phone、email 添加 unique contraint
    # (主要是 Django 里的实现,如果要添加 unique contraint,那么 phone、email
    # 比如允许为 null,但 django 中 email 并未声明允许为 null,一些地方的代码
    # 也有这样的假定,因此我们尽量不打破这种惯例)
    # 我们手动判断邮箱或手机号是否存在,如果存在则报错。这会存在 race condition,
    # 有可能两个并发的请求导致创建了邮箱或手机号相同的账号,不过这种概率应该很低。
    # 除非两个请求同时运行上述 verify_code 并且都返回了 True (极小概率时间),
    # 而且这里又同时读取数据库、创建新用户,才会导致问题。因此暂时不考虑这个问题。

    if phone:
        if User.objects.filter(phone=phone, phone_validated=True).exists():
            return errors.PhoneExists()
        user = User.objects.create_user(phone=phone,
                                        phone_validated=True,
                                        password=password)
    else:
        if User.objects.filter(email=email, email_validated=True).exists():
            return errors.EmailExists()
        user = User.objects.create_user(email=email,
                                        email_validated=True,
                                        password=password)

    return api.ok(data=serialize_user(user))