diff --git a/README.md b/README.md index 83c478721b786e6158e4a7fe4e3667940a9aa0a4..2470c974ca8272ae3c9204dbe1972f5df04e5983 100644 --- a/README.md +++ b/README.md @@ -9,4 +9,10 @@ 在V1.0的基础上进一步实现CMS用户登录、错误信息返回、登录限制和CSRF保护、CMS用户名渲染和注销、CMS个人页面和模板抽离等功能,进一步丰富后台管理功能。 #### V1.2 -在V1.1的基础上实现后台修改密码布局、通过Ajax实现局部更新修改密码、优化Json数据返回、sweetalert美化提示框、修改邮箱界面搭建功能。 \ No newline at end of file +在V1.1的基础上实现后台修改密码布局、通过Ajax实现局部更新修改密码、优化Json数据返回、sweetalert美化提示框、修改邮箱界面搭建功能。 + +#### V1.3 +在V1.2的时候进一步完善,首先实现在Flask中发送邮件,并进一步定义发送验证码,并进一步实现修改邮箱,还对权限和角色模型进行了定义。 + +注意: +在配置文件config.md中需要将自己的邮箱信息输入,才能正常实现其功能。 \ No newline at end of file diff --git a/__pycache__/bbs.cpython-37.pyc b/__pycache__/bbs.cpython-37.pyc index 1565c985d70c85eefe392a043c03a64f20ee6cdd..c3e0c93d0a61e54f08b1452ad577153489dcbeb3 100644 Binary files a/__pycache__/bbs.cpython-37.pyc and b/__pycache__/bbs.cpython-37.pyc differ diff --git a/__pycache__/config.cpython-37.pyc b/__pycache__/config.cpython-37.pyc index 0182dafded4ec66f58274cff5085805a43e7a128..7b267f4e5121105fb21b8b6d07706955f938fe93 100644 Binary files a/__pycache__/config.cpython-37.pyc and b/__pycache__/config.cpython-37.pyc differ diff --git a/__pycache__/exts.cpython-37.pyc b/__pycache__/exts.cpython-37.pyc index 152bcd752c884be9e1f54bf2f2ff40469f3df2b7..b344fe0fde0cb11aec8966f9db7f91f476ba3729 100644 Binary files a/__pycache__/exts.cpython-37.pyc and b/__pycache__/exts.cpython-37.pyc differ diff --git a/apps/cms/__pycache__/forms.cpython-37.pyc b/apps/cms/__pycache__/forms.cpython-37.pyc index a138161c24758284592bf95956acd4b2b9cc6d1f..6410862a744dcc1003ca2e2df60ef1adb0ec3162 100644 Binary files a/apps/cms/__pycache__/forms.cpython-37.pyc and b/apps/cms/__pycache__/forms.cpython-37.pyc differ diff --git a/apps/cms/__pycache__/models.cpython-37.pyc b/apps/cms/__pycache__/models.cpython-37.pyc index f0af941081ee0ba3cbabbf4378ac8cfd265c2ecd..e4ad6e93fe1d448584a0cae4cd74b530e78414b9 100644 Binary files a/apps/cms/__pycache__/models.cpython-37.pyc and b/apps/cms/__pycache__/models.cpython-37.pyc differ diff --git a/apps/cms/__pycache__/views.cpython-37.pyc b/apps/cms/__pycache__/views.cpython-37.pyc index 2f049967165481a6e02bbd328a2d0b8022c454f6..c7dfada11ffcdb7e7809d4d27718e18662c2fb02 100644 Binary files a/apps/cms/__pycache__/views.cpython-37.pyc and b/apps/cms/__pycache__/views.cpython-37.pyc differ diff --git a/apps/cms/forms.py b/apps/cms/forms.py index de7c6fac47d760bc83ed804edd5d06cd584a3bc4..98477f5b50d962d6f00bb1b347efcc23270b5925 100644 --- a/apps/cms/forms.py +++ b/apps/cms/forms.py @@ -1,5 +1,6 @@ -from wtforms import Form, StringField, IntegerField +from wtforms import Form, StringField, IntegerField, ValidationError from wtforms.validators import Email, InputRequired, Length, EqualTo +from utils import clcache class BaseForm(Form): @@ -17,4 +18,16 @@ class LoginForm(BaseForm): class ResetPwdForm(BaseForm): oldpwd = StringField(validators=[Length(6, 20, message='密码长度应介于6-20')]) newpwd = StringField(validators=[Length(6, 20, message='密码长度应介于6-20')]) - newpwd2 = StringField(validators=[EqualTo("newpwd", message='两次输入密码不一致')]) \ No newline at end of file + newpwd2 = StringField(validators=[EqualTo("newpwd", message='两次输入密码不一致')]) + + +class ResetEmailForm(BaseForm): + email = StringField(validators=[Email(message='您的邮箱格式有误,请重新输入')]) + captcha = StringField(validators=[Length(4, 4, message='请输入4位验证码')]) + + def validate_captcha(self, form): + captcha = self.captcha.data + email = self.email.data + redis_captcha = clcache.get_captcha(email) + if not redis_captcha or captcha.lower() != redis_captcha.lower(): + raise ValidationError('邮箱验证码错误') \ No newline at end of file diff --git a/apps/cms/models.py b/apps/cms/models.py index a5b91406b2a6ce63e22d231243085e80e682be81..64757efbaad8c909ec4db11450cc40ef07e65ef6 100644 --- a/apps/cms/models.py +++ b/apps/cms/models.py @@ -30,4 +30,47 @@ class CMSUser(db.Model): def check_password(self, raw_password): '''验证密码是否正确''' result = check_password_hash(self.password, raw_password) - return result \ No newline at end of file + return result + + +class CMSPermission(object): + # 255二进制表示所有权限 + ALL_PERMISSION = 0b11111111 + + # 访问权限 + VISITOR = 0b00000001 + + # 管理帖子权限 + POSTER = 0b00000010 + + # 管理评论权限 + COMMENTER = 0b00000100 + + # 管理板块 + BOARDER = 0b00001000 + + # 管理前台用户 + FRONTUSER = 0b00010000 + + # 管理前台用户 + CMSUSER = 0b00100000 + + # 管理前台用户 + ADMINER = 0b01000000 + + +cms_role_user = db.Table( + 'cms_role_user', + db.Column('cms_role_id', db.Integer, db.ForeignKey('cms_role.id'), primary_key=True), + db.Column('cms_user_id', db.Integer, db.ForeignKey('cms_user.id'), primary_key=True) +) + + +class CMSRole(db.Model): + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + name = db.Column(db.String(50), nullable=False) + desc = db.Column(db.String(200), nullable=False) + create_time = db.Column(db.DateTime, default=datetime.now()) + permissions = db.Column(db.Integer, default=CMSPermission.VISITOR) + + users = db.relationship('CMSUser', secondary=cms_role_user, backref='roles') \ No newline at end of file diff --git a/apps/cms/views.py b/apps/cms/views.py index e9590333078f9c67f1505e0b7813a2ee728eb8a6..7b908938c07bc0361f0baa33fcfcf738ace3d8c7 100644 --- a/apps/cms/views.py +++ b/apps/cms/views.py @@ -1,8 +1,9 @@ from flask import Blueprint, render_template, views, request, redirect, url_for, session, g -from apps.cms.forms import LoginForm, ResetPwdForm +from flask_mail import Message +from apps.cms.forms import LoginForm, ResetPwdForm, ResetEmailForm from apps.cms.models import CMSUser -from exts import db -from utils import restful +from exts import db, mail +from utils import restful, random_captcha, clcache # from .decorators import login_required # .代表当前路径 @@ -87,9 +88,35 @@ class ResetEmailView(views.MethodView): return render_template('cms/cms_resetemail.html') def post(self): - pass + form = ResetEmailForm(request.form) + if form.validate(): + email = form.email.data + # 修改用户邮箱 + g.cms_user.email = email + db.session.commit() + return restful.success() + else: + return restful.params_error(form.get_error()) + + +class EmailCaptchaView(views.MethodView): + def get(self): + email = request.args.get('email') + if not email: + return restful.params_error('请传递邮箱参数') + # 发送邮件验证码,可以是4位或6位的数字与英文组合 + captcha = random_captcha.get_random_captcha(4) + try: + message = Message('熊熊论坛验证码', recipients=[email], body='您正在进行更改邮箱验证,验证码是%s,5分钟内有效,请及时输入、注意保密。' % captcha) + mail.send(message) + except Exception as e: + print(e.args[0]) + return restful.server_error('邮件发送异常,请检查重试') + clcache.save_captcha(email, captcha) + return restful.success(message='邮件发送成功,请注意接收验证码') cms_bp.add_url_rule('/login/', view_func=LoginView.as_view('login')) cms_bp.add_url_rule('/resetpwd/', view_func=ResetPwdView.as_view('resetpwd')) cms_bp.add_url_rule('/resetemail/', view_func=ResetEmailView.as_view('resetemail')) +cms_bp.add_url_rule('/email_captcha/', view_func=EmailCaptchaView.as_view('email_captcha')) diff --git a/bbs.py b/bbs.py index 75d952a675adc0df8ad052e35a4b6c05899bba21..d96161dd1efb9e11bf560d5190e6bbd44ceeb663 100644 --- a/bbs.py +++ b/bbs.py @@ -6,7 +6,7 @@ from flask import Flask from flask_wtf import CSRFProtect -from exts import db +from exts import db, mail from apps.cms.views import cms_bp from apps.front.views import front_bp import config @@ -15,8 +15,8 @@ import config app = Flask(__name__) CSRFProtect(app) app.config.from_object(config) -app.config['TEMPLATE_AUTO_RELOAD'] = True db.init_app(app) +mail.init_app(app) app.register_blueprint(cms_bp) app.register_blueprint(front_bp) diff --git a/config.py b/config.py index ec2cd3e4db5bd6b631d53a7abd8f964595ed3be2..034f12ef9b74ef57ee3b5d52f5ffc004bbc70524 100644 --- a/config.py +++ b/config.py @@ -8,10 +8,24 @@ PASSWORD = 'root' DATABASE = 'flask_bbs' DB_URL = 'mysql+mysqlconnector://{}:{}@{}:{}/{}?charset=utf8'.format(USERNAME, PASSWORD, HOSTNAME, PORT, DATABASE) +TEMPLATE_AUTO_RELOAD = True SQLALCHEMY_DATABASE_URI = DB_URL SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_POOL_RECYCLE = 280 SQLALCHEMY_POOL_SIZE = 20 # 设置密钥 -SECRET_KEY = os.urandom(15) \ No newline at end of file +SECRET_KEY = os.urandom(15) + +# 发送邮箱服务地址 +MAIL_SERVER = 'smtp.qq.com' +# 邮箱端口,为587或465,为587时TLS设置为True,为465时SSL设置为True +MAIL_PORT = 587 +MAIL_USE_TLS = True +# MAIL_USE_SSL = True # MAIL_PORT为465时设置此项 +# 用户名可以为你的邮箱,需要自行添加 +MAIL_USERNAME = '123456789@qq.com' +# 邮箱密码,不是邮箱账号密码,而是第三方客户端登录使用的授权码,需要自行获取 +MAIL_PASSWORD = 'xxxxxxxxxxxxxxxx' +# 发送者即你的邮箱,需要自行添加 +MAIL_DEFAULT_SENDER = '123456789@qq.com' \ No newline at end of file diff --git a/exts.py b/exts.py index 2e1eeb63ff7528d9ae956c7700724bf9989c1fe1..2f87add254338173835f5115c4822b1fb2c4ff41 100644 --- a/exts.py +++ b/exts.py @@ -1,3 +1,6 @@ from flask_sqlalchemy import SQLAlchemy +from flask_mail import Mail -db = SQLAlchemy() \ No newline at end of file +db = SQLAlchemy() + +mail = Mail() \ No newline at end of file diff --git a/manage.py b/manage.py index fe652a758f7d11ee1cbfe65d03600ce2fe5113d1..5c32591497bc27c3a7a1369f9135ce758457d1c1 100644 --- a/manage.py +++ b/manage.py @@ -2,7 +2,7 @@ from flask_script import Manager from bbs import app from flask_migrate import Migrate, MigrateCommand from exts import db -from apps.cms.models import CMSUser +from apps.cms.models import CMSUser, CMSRole manager = Manager(app) Migrate(app, db) diff --git a/migrations/versions/__pycache__/ba80d307aa21_.cpython-37.pyc b/migrations/versions/__pycache__/ba80d307aa21_.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..108b9ec3bcb75c7d59248158beaaefedf21c4ec3 Binary files /dev/null and b/migrations/versions/__pycache__/ba80d307aa21_.cpython-37.pyc differ diff --git a/migrations/versions/ba80d307aa21_.py b/migrations/versions/ba80d307aa21_.py new file mode 100644 index 0000000000000000000000000000000000000000..76dfbf5ad7030ecdccb783f6dd5106074b9174b4 --- /dev/null +++ b/migrations/versions/ba80d307aa21_.py @@ -0,0 +1,43 @@ +"""empty message + +Revision ID: ba80d307aa21 +Revises: f7a47bdbaa71 +Create Date: 2020-06-05 20:35:29.305779 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'ba80d307aa21' +down_revision = 'f7a47bdbaa71' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('cms_role', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('name', sa.String(length=50), nullable=False), + sa.Column('desc', sa.String(length=200), nullable=False), + sa.Column('create_time', sa.DateTime(), nullable=True), + sa.Column('permissions', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('cms_role_user', + sa.Column('cms_role_id', sa.Integer(), nullable=False), + sa.Column('cms_user_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['cms_role_id'], ['cms_role.id'], ), + sa.ForeignKeyConstraint(['cms_user_id'], ['cms_user.id'], ), + sa.PrimaryKeyConstraint('cms_role_id', 'cms_user_id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('cms_role_user') + op.drop_table('cms_role') + # ### end Alembic commands ### diff --git a/requirements.txt b/requirements.txt index 94229556a09b80241dfee36cca4dbf8e07c53480..1362bbceb1756233cf34d116364b70adf788ade7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,6 +23,7 @@ PyMySQL==0.9.3 python-dateutil==2.8.1 python-editor==1.0.4 pytz==2020.1 +redis==3.5.3 six==1.14.0 SQLAlchemy==1.3.16 Werkzeug==1.0.1 diff --git a/static/cms/css/cms_base.css b/static/cms/css/cms_base.css index e45c29a930af4d71fef5ce01aad4db872688323f..5a176924dca37f5804c001bd5eeba9c9ae4aecd8 100644 --- a/static/cms/css/cms_base.css +++ b/static/cms/css/cms_base.css @@ -195,4 +195,8 @@ body { border-radius: 2px; background: #ecedf0; overflow: hidden; +} + +.form-container { + width: 300px; } \ No newline at end of file diff --git a/static/cms/js/resetemail.js b/static/cms/js/resetemail.js index ed4ed45db328d93d17f526887ddb223282863fa8..924d71270be2a9665a7f719b50b86c6fff410980 100644 --- a/static/cms/js/resetemail.js +++ b/static/cms/js/resetemail.js @@ -1,4 +1,4 @@ -var lgajax = { +var clajax = { 'get': function (args) { args['method'] = 'get'; this.ajax(args); @@ -28,10 +28,10 @@ $(function () { event.preventDefault(); var email = $("input[name='email']").val(); if (!email) { - lgalert.alertInfoToast('请输入邮箱'); + clalert.alertInfoToast('请输入邮箱'); return; } - var lgajax = { + var clajax = { 'get': function (args) { args['method'] = 'get'; this.ajax(args); @@ -56,20 +56,20 @@ $(function () { }); } }; - lgajax.get({ + clajax.get({ 'url': '/cms/email_captcha/', 'data': { 'email': email }, 'success': function (data) { - if (data['code'] == 200) { - lgalert.alertSuccessToast('邮件发送成功!请注意查收!'); + if (data['code'] === 200) { + clalert.alertSuccessToast('邮件发送成功!请注意查收!'); } else { - lgalert.alertInfo(data['message']); + clalert.alertInfo(data['message']); } }, 'fail': function (error) { - lgalert.alertNetworkError(); + clalert.alertNetworkError(); } }); }); @@ -84,23 +84,23 @@ $(function () { var email = emailE.val(); var captcha = captchaE.val(); - lgajax.post({ + clajax.post({ 'url': '/cms/resetemail/', 'data': { 'email': email, 'captcha': captcha }, 'success': function (data) { - if (data['code'] == 200) { + if (data['code'] === 200) { emailE.val(""); captchaE.val(""); - lgalert.alertSuccessToast('恭喜!邮箱修改成功!'); + clalert.alertSuccessToast('恭喜!邮箱修改成功!'); } else { - lgalert.alertInfo(data['message']); + clalert.alertInfo(data['message']); } }, 'fail': function (error) { - lgalert.alertNetworkError(); + clalert.alertNetworkError(); } }); }); diff --git a/templates/cms/cms_resetemail.html b/templates/cms/cms_resetemail.html index b7d5a4f8635d77a6416e0d5a759e9325e7d5eac5..17e8d38a7ca75b82fd113329ee69a85a10bf21ff 100644 --- a/templates/cms/cms_resetemail.html +++ b/templates/cms/cms_resetemail.html @@ -9,11 +9,7 @@ {% endblock %} {% block head %} - + {% endblock %} {% block content %} diff --git a/templates/cms/cms_resetpwd.html b/templates/cms/cms_resetpwd.html index 515fc677707b12ad98c92be077f69e599fe1f293..8acfd683115c21de32fce552f4e4c8f8afb574e1 100644 --- a/templates/cms/cms_resetpwd.html +++ b/templates/cms/cms_resetpwd.html @@ -9,11 +9,6 @@ {% endblock %} {% block head %} - {% endblock %} diff --git a/utils/__pycache__/clcache.cpython-37.pyc b/utils/__pycache__/clcache.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0db45f82223afdd6daa0e05c45cea8e2b3a33475 Binary files /dev/null and b/utils/__pycache__/clcache.cpython-37.pyc differ diff --git a/utils/__pycache__/random_captcha.cpython-37.pyc b/utils/__pycache__/random_captcha.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..197caa29e14d21205b1ab903a7d3e51072361f7c Binary files /dev/null and b/utils/__pycache__/random_captcha.cpython-37.pyc differ diff --git a/utils/clcache.py b/utils/clcache.py new file mode 100644 index 0000000000000000000000000000000000000000..93704d2d736c2300dbe52a4d4ec78d25d3315592 --- /dev/null +++ b/utils/clcache.py @@ -0,0 +1,18 @@ +import redis + +# 连接Redis数据库 +r = redis.StrictRedis(host='localhost', port=6379, db=0, decode_responses=True) + + +def save_captcha(key, value, timeout=300): + '''把验证码存到Redis''' + return r.set(key, value, timeout) + + +def get_captcha(key): + '''从Redis中取验证码''' + return r.get(key) + + +def delete_captcha(key): + return r.delete(key) diff --git a/utils/random_captcha.py b/utils/random_captcha.py new file mode 100644 index 0000000000000000000000000000000000000000..157e6d9451a2dd0a1c35b65c58da5bd348e84789 --- /dev/null +++ b/utils/random_captcha.py @@ -0,0 +1,14 @@ +import random + + +def get_random_captcha(num): + '''生成随机验证码''' + code = '' + for i in range(num): + num = str(random.randint(0, 9)) + upper = chr(random.randint(65, 90)) + lower = chr(random.randint(97, 122)) + lst = [num, upper, lower] + ret = random.choice(lst) + code = ''.join([code, ret]) + return code \ No newline at end of file