更多創客作品,請關注筆者網站園丁鳥,蒐集全球極具創意,且有價值的創客作品
ROS機器人知識請關注,diegorobot
業餘時間完成的一款在線統計過程分析工具SPC,及SPC知識分享網站qdo
前言
對於多租戶的SAAS系統,所有的操作都是以組織爲單位的,所以相對於傳統的單用戶系統的用戶權限管理,增加了一層組織的維度,一個註冊企業下,又可以有完整的用戶權限管理系統。
數據模型設計
如下是用權限系統的關係圖:
組織在SAAS系統中的一切資源的最高階組織形式,所以其他的對象都應該有一個組織的屬性,對於用戶也是如從,應該屬於某個組織,組織與用戶的關係應該是一對多的關係,如下是組織的Model對象。
class Organization(db.Model):
"""
Create a Organization table
"""
__tablename__ = 'organizations'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
key=db.Column(db.String(64), unique=True)
country = db.Column(db.String(64))
state = db.Column(db.String(64))
city = db.Column(db.String(64))
address = db.Column(db.String(64))
status = db.Column(db.Integer)#0:disable,1:enable,2:temp for first register
description = db.Column(db.String(200))
users = db.relationship('User', backref='Organization',
lazy='dynamic',cascade='all, delete-orphan', passive_deletes = True)
用戶用戶可以登錄到系統進行相關功能的操作,每個用戶都屬於某個組織,可以根據權限操作此組織下的資源,數據。每個用戶都有一組角色信息,根據角色來判斷其權限,如下是用戶的model,由於後續將使用Flask-login進行用戶登錄註冊的管理,所以User類繼承自UserMixin,在其中擴展了Organization_id與組織相關聯,其他的屬性可以根據需求進行擴展。
class User(UserMixin, db.Model):
"""
Create an User table
"""
# Ensures table will be named in plural and not in singular
# as is the name of the model
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(60), index=True, unique=True)
username = db.Column(db.String(60), index=True)
first_name = db.Column(db.String(60), index=True)
last_name = db.Column(db.String(60), index=True)
mobilephone = db.Column(db.String(20),index=True)
password_hash = db.Column(db.String(128))
active = db.Column(db.Boolean())
confirmed_at = db.Column(db.DateTime())
organization_id = db.Column(db.Integer, db.ForeignKey('organizations.id'))
status = db.Column(db.Integer)#0:disable,1:enable
avatar = db.Column(db.String(60))# avatar pic name
roles_user = db.relationship('Role_User', backref='User',
lazy='dynamic',cascade='all, delete-orphan', passive_deletes = True)
@property
def password(self):
"""
Prevent pasword from being accessed
"""
raise AttributeError(_('password is not a readable attribute.'))
@password.setter
def password(self, password):
"""
Set password to a hashed password
"""
self.password_hash = generate_password_hash(password)
def check_password_hash(self, password):
return check_password_hash(self.password_hash,password)
def verify_password(self, password):
"""
Check if hashed password matches actual password
"""
return check_password_hash(self.password_hash, password)
def has_permission(self,permission):
"""
Check if hashed the permission
"""
for ru in self.roles_user:
role=Role.query.filter(Role.id==ru.role_id).first()
if role.name==permission:
return True
return False
has_permission函數根據權限名稱來檢索此用戶是否有對應的角色權限
角色:每個角色表示一組操作的權限,可以操作系統相應的資源數據,用戶與角色是多對多的關係,如下是角色的model,與用戶類對象通過Role_User表進行關聯。
class Role(db.Model):
"""
Create a Role table
"""
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(60))
status = db.Column(db.Integer)#0:disable,1:enable
description = db.Column(db.String(100))
caption = db.Column(db.String(60))
users = db.relationship('Role_User', backref='Role',
lazy='dynamic',cascade='all, delete-orphan', passive_deletes = True)
def __repr__(self):
return '<Role: {}>'.format(self.name)
class Role_User(db.Model):
"""
Create a Role_User table
"""
__tablename__ = 'role_users'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
def __repr__(self):
return '<Role_User: {}>'.format(self.name)
在本系統中設立如下三種角色:
- Admin,企業的系統管理員,可以進行所有的操作,而且可以新建新的用戶
- Generic user,普通用戶,只能進行功能性的操作,如SPC分析,田口實驗設計
- Resource Admin,資源管理,可以對企業資源進行管理,如設備,生產線,實驗室等
頁面表單form設計
對用戶的操作,我們主要有如下三個操作,每個操作都對應相應的form。
- 用戶登錄
用戶通過email,和密碼來登錄,同時有remember me的屬性,所以只需要此三給元素即可,而且都爲必填字段。同時有validate_email函數對輸入email進行驗證,判斷是否已經註冊,對於輸入數據格式的驗證本系統中都在前端網頁驗證。
class LoginForm(FlaskForm):
"""
Form for users to login
"""
email = StringField('Email Address', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired(message= _('the password can not be null.'))])
remember_me = BooleanField('Remember me')
submit = SubmitField('Sign In')
def validate_email(self, field):
if not User.query.filter_by(email=field.data).first():
raise ValidationError(_('Invalid email.'))
- 用戶註冊
在這裏我們只通過簡單的郵件地址註冊,並對輸入的密碼進行兩次校驗,所以驗證函數有三個,分別對郵件是否已被註冊,是否同意用戶協議,兩次輸入密碼是否一致進行校驗。
class RegistrationForm(FlaskForm):
"""
Form for users to create new account
"""
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired(message= _('the password can not be null.'))])
password_again = PasswordField('Password again', validators=[DataRequired(message= _('the password again can not be null.'))])
agree_policy = BooleanField('Agree Policy')
submit = SubmitField('Register')
def validate_email(self, field):
if User.query.filter_by(email=field.data).first():
raise ValidationError(_('Email is already in use.'))
def validate_agree_policy(self, field):
if not field.data:
raise ValidationError('you must agree the policy.')
def validate_password_again(self, field):
if field.data!=self._fields['password'].data:
raise ValidationError('Inconsistent password twice')
- 新建/編輯用戶
企業管理員可以新建用戶,並分配角色,對已有用戶進行編輯,角色修改,對應於UserForm,此form類中對用戶名稱,郵件地址,移動電話,角色都進行驗證,保證相關數據的正確性。同時在這裏增加了對企業license中用戶數量進行驗證
class UserForm(FlaskForm):
"""
Form for edit user
"""
user_id = IntegerField('id')
email = StringField('email', validators=[DataRequired()])
username = StringField('username', validators=[DataRequired()])
mobilephone = StringField('mobile phone')
password = PasswordField('password')
is_admin = StringField('is admin')
is_resource_admin = StringField('is resource admin')
is_generic_user = StringField('is generic user')
submit = SubmitField('Submit')
def validate_email(self, field):
if self._fields['user_id'].data == -1:
if User.query.filter(User.email==field.data).first():
raise ValidationError(_('The user email is already in use.'))
else:
if User.query.filter(User.email==field.data,User.id!=self._fields['user_id'].data).first():
raise ValidationError(_('The user email is already in use.'))
def validate_mobilephone(self, field):
if self._fields['user_id'].data == -1:
if User.query.filter(User.mobilephone==field.data,User.organization_id==current_user.organization_id).first():
raise ValidationError(_('The user mobilephone is already in use.'))
else:
if User.query.filter(User.mobilephone==field.data,User.organization_id==current_user.organization_id,User.id!=self._fields['user_id'].data).first():
raise ValidationError(_('The user mobilephone is already in use.'))
def validate_password(self, field):
if self._fields['user_id'].data == -1:
if field.data is None:
raise ValidationError(_('the password can not be null.'))
def validate_is_admin(self, field):
if self._fields['user_id'].data == -1:
if field.data=='is_admin':
licenses =Organization_License.query.filter(Organization_License.organization_id==current_user.organization_id,Organization_License.license_id==License.id,License.name=='admin_numbers').first()
quantity =Role_User.query.filter(Role_User.user_id==current_user.id,Role_User.role_id==Role.id,Role.name=='admin').count()
if quantity>=licenses.quantity:
raise ValidationError(_('the numbers of admin you have created has more than the numbers of your license.'))
def validate_is_resource_admin(self, field):
if self._fields['user_id'].data == -1:
if field.data=='is_resource_admin':
licenses =Organization_License.query.filter(Organization_License.organization_id==current_user.organization_id,Organization_License.license_id==License.id,License.name=='resource_admin_numbers').first()
quantity =Role_User.query.filter(Role_User.user_id==current_user.id,Role_User.role_id==Role.id,Role.name=='admin').count()
if quantity>=licenses.quantity:
raise ValidationError(_('the numbers of resource admin you have created has more than the numbers of your license.'))
def validate_is_generic_user(self, field):
if self._fields['user_id'].data == -1:
if field.data=='is_generic_user':
licenses =Organization_License.query.filter(Organization_License.organization_id==current_user.organization_id,Organization_License.license_id==License.id,License.name=='generic_user_numbers').first()
quantity =Role_User.query.filter(Role_User.user_id==current_user.id,Role_User.role_id==Role.id,Role.name=='admin').count()
if quantity>=licenses.quantity:
raise ValidationError(_('the numbers of generic user you have created has more than the numbers of your license.'))
前端Web頁面設計
前端頁面都使用bootstrap進行設計,對應數據格式的常規驗證都在前端進行,都是基本的h5頁面,這裏只貼代碼
登錄頁面
<body class="authentication-bg">
<div class="account-pages mt-5 mb-1">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-5">
<div class="card mb-0">
<!-- Logo -->
<div class="card-body pt-4 pb-0 text-center">
<a href="index.html">
<span><img src="/static/assets/images/logo-qdo-dark.png" alt=""></span>
</a>
</div>
<div class="card-body p-4">
<form method="post" action="/login" name="login" class="needs-validation" novalidate>
{{ form.csrf_token }}
<div class="form-group">
<input class="form-control" type="email" id="email" name="email" required placeholder="{{_('Enter your email')}}">
<div class="invalid-feedback">
{{_('Please provide a valid email.')}}
</div>
</div>
<div class="form-group">
<input class="form-control" type="password" required id="password" name="password" placeholder="{{_('Enter your password')}}">
<div class="invalid-feedback">
{{_('Please input the correct password.')}}
</div>
</div>
<div class="form-group mb-3">
<a href="pages-recoverpw.html" class="text-muted float-right"><small>{{_('Forgot your password?')}}</small></a>
<div class="custom-control custom-checkbox">
<input type="checkbox" name="remember_me" class="custom-control-input" id="checkbox-signin" checked>
<label class="custom-control-label" for="checkbox-signin">{{_('Remember me')}}</label>
</div>
</div>
{% if form.errors %}
<ul class="errors" style="color:#FF0033">
{% for field_name, field_errors in form.errors|dictsort if field_errors %}
{% for error in field_errors %}
{{ form[field_name].label }}: {{ error }}
{% endfor %}
{% endfor %}
</ul>
{% endif %}
<div class="row justify-content-center">
<div class="col-8">
<div class="form-group mb-0">
<button class="btn btn-primary btn-block" type="submit" name="Sign in">{{_(' Log In ')}}</button>
</div>
</div>
</div>
</form>
<div class="row mt-3">
<div class="col-12 text-center">
<p class="text-muted mb-0">{{_('Do not have an account?')}} <a href="{{ url_for('auth.register') }}" class="text-muted ml-1"><b>{{_('Sign Up')}}</b></a></p>
</div>
</div>
</div>
</div>
</div> <!-- end col -->
</div>
<!-- end row -->
</div>
<!-- end container -->
</div>
<!-- end page -->
<!-- Required js -->
<script src="/static/assets/js/app.js"></script>
</body>
註冊頁面
<body class="authentication-bg">
<div class="account-pages mt-5 mb-1">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-5">
<div class="card mb-0">
<!-- Logo-->
<div class="card-body pt-4 pb-0 text-center">
<a href="index.html">
<span><img src="/static/assets/images/logo-qdo-dark.png" alt=""></span>
</a>
</div>
<div class="card-body p-4">
<form method="post" action="/register" class="needs-validation" novalidate>
{{ form.csrf_token }}
<div class="form-group">
<input class="form-control" type="email" id="email" name="email" required placeholder="{{_('Enter your email')}}">
<div class="invalid-feedback">
{{_('Please provide a valid email.')}}
</div>
</div>
<div class="form-group">
<input class="form-control" type="password" required id="password" name="password" placeholder="{{_('Enter your password')}}">
<div class="invalid-feedback">
{{_('Please input the correct password.')}}
</div>
</div>
<div class="form-group">
<input class="form-control" type="password" required id="password_again" name="password_again" placeholder="{{_('Enter your password again')}}">
<div class="invalid-feedback">
{{_('Please input the correct password again.')}}
</div>
</div>
<div class="form-group">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" name="agree_policy" checked id="agree_policy">
<label class="custom-control-label" for="agree_policy">{{_('I accept')}} <a href="#" class="text-muted">{{_('Terms & Conditions')}}</a></label>
<div class="invalid-feedback">
{{_('Please acceept the agree policy')}}
</div>
</div>
</div>
{% if form.errors %}
<ul class="errors">
{% for field_name, field_errors in form.errors|dictsort if field_errors %}
{% for error in field_errors %}
<span style="color:#FF0033">
<li>{{ form[field_name].label }}: {{ error }}</li>
</span>
{% endfor %}
{% endfor %}
</ul>
{% endif %}
<div class="row justify-content-center">
<div class="col-8">
<div class="form-group mb-0">
<button class="btn btn-primary btn-block" type="submit"> {{_('Sign Up')}} </button>
</div>
</div>
</div>
</form>
<div class="row mt-3">
<div class="col-12 text-center">
<p class="text-muted">{{_('Already have account?')}} <a href="{{ url_for('auth.login') }}" class="text-muted ml-1"><b>{{_('Log In')}}</b></a></p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- end container -->
</div>
<!-- end page -->
<!-- Required js -->
<script src="/static/assets/js/app.js"></script>
</body>
新建/編輯用戶
{% extends "layout.html" %}
{% block content %}
<!-- Start Content-->
<div class="container-fluid">
<!-- start page title -->
<div class="row">
<div class="col-12">
<div class="page-title-box">
<div class="page-title-right">
<ol class="breadcrumb m-0">
<li class="breadcrumb-item"><a href="javascript: void(0);"><i class="feather icon-home"></i></a></li>
<li class="breadcrumb-item"><a href="javascript: void(0);">{{_('Auth')}}</a></li>
<li class="breadcrumb-item active">{{_('Users')}}</li>
</ol>
</div>
<h4 class="page-title">{{_('New User')}}</h4>
</div>
</div>
</div>
<!-- end page title -->
<!-- form -->
<div class="card">
<div class="card-header">
<h5>{{_('Info')}}</h5>
</div>
<div class="card-body">
<form class="needs-validation" novalidate method="post" action="{{ url_for('auth.user_add')}}" name="user_add">
{{ form.csrf_token }}
<div class="form-row">
<div class="form-group col-md-4">
<input type="hidden" id="user_id" name="user_id" value="-1" />
<label for="username">{{_('Name')}}</label>
<input class="form-control" type="text" id="username" name="username" required placeholder={{_('Enter your name')}}>
<div class="invalid-feedback">
{{_('Please input a valid name.')}}
</div>
</div>
<div class="form-group col-md-8">
<label for="email">{{_('Email address')}}</label>
<input class="form-control" type="email" id="email" name="email" required placeholder={{_('Enter your email')}}>
<div class="invalid-feedback">
{{_('Please input a valid email.')}}
</div>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<label for="mobilephone">{{_('Mobile Phone')}}</label>
<input class="form-control" type="text" id="mobilephone" name="mobilephone" required placeholder={{_('Enter your mobilephone')}}>
<div class="invalid-feedback">
{{_('Please input a valid mobile phone number.')}}
</div>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<label for="password">{{_('Default Password')}}</label>
<input class="form-control" type="password" required id="password" name="password" placeholder={{_('Enter your password')}}>
<div class="invalid-feedback">
Please input the correct password.')}}
</div>
</div>
</div>
<hr>
<div class="form-group col-md-12">
<input type="checkbox" class="form-check-input" id="is_admin" name="is_admin" value="is_admin">
<span class="badge badge-danger">{{_('is Admin')}}</span>
<label class="form-check-label" for="c_user.has_permission('admin')">{{_('It has the whole permissions.')}}</label>
</div>
<div class="form-group col-md-12">
<input type="checkbox" class="form-check-input" id="is_resource_admin" name="is_resource_admin" value="is_resource_admin">
<span class="badge badge-info">{{_('is Resource Admin')}}</span>
<label class="form-check-label" for="is_resource_admin">{{_('It can manage the resource, like eqp, line, defect...')}}</label>
</div>
<div class="form-group col-md-12">
<input type="checkbox" class="form-check-input" id="is_generic_user" name="is_generic_user" value="is_generic_user">
<span class="badge badge-dark">{{_('is Generic User')}}</span>
<label class="form-check-label" for="is_generic_user">{{_('It can edit and view the quality data')}}</label>
</div>
{% if form.errors %}
<ul class="errors" style="color:#FF0033">
{% for field_name, field_errors in form.errors|dictsort if field_errors %}
{% for error in field_errors %}
{{ form[field_name].label }}: {{ error }}
{% endfor %}
{% endfor %}
</ul>
{% endif %}
{% if c_user.status!=0 %}
<button type="submit" class="btn btn-primary float-right">{{_('Submit')}}</button>
{% else %}
<button type="submit" class="btn btn-primary float-right" disabled>{{_('Submit')}}</button>
{% endif %}
</form>
</div>
</div>
</div> <!-- container -->
{% endblock %}
功能邏輯
註冊登錄都使用Flask-login,只是在註冊頁面,增加了爲新用戶新建一個臨時的組織信息。
@auth.route('/register', methods=['GET', 'POST'])
def register():
"""
Handle requests to the /register route
Add an user to the database through the registration form
"""
form = RegistrationForm()
if form.validate_on_submit():
new_organization=Organization();
new_organization.name=form.email.data
new_organization.description=form.email.data
new_organization.country="China"
new_organization.state="ShangHai"
new_organization.city="ShangHai"
new_organization.address="ShangHai"
new_organization.status=2#temp status
new_organization.key=str(uuid.uuid4()).upper().replace('-','')
db.session.add(new_organization)
db.session.flush()
user = User(email=form.email.data,username=form.email.data,password=form.password.data,organization_id=new_organization.id,status=1)
db.session.add(user)
db.session.flush()
role = Role.query.filter(Role.name=='admin').first()
user_role=Role_User(user_id=user.id,role_id=role.id)
db.session.add(user_role)
db.session.commit()
flash(_('You have successfully registered! You may now login.'))
# redirect to the login page
return redirect(url_for('auth.login'))
# load registration template
return render_template('register.html', form=form, title='Register')
新建用戶
新建用戶操作是由組織管理員完成,需要添加用戶信息,同時爲用戶分配的角色,需要操作user,role_user兩張表
@auth.route('/users/add', methods=['GET', 'POST'])
@login_required
def user_add():
"""
Add a users to the database
"""
form = UserForm()
if form.validate_on_submit():
user = User(email=form.email.data,username=form.username.data,password=form.password.data,
mobilephone=form.mobilephone.data,
organization_id=current_user.organization_id)
try:
# add user to the database
db.session.add(user)
db.session.flush()
if form.is_admin.data=='is_admin':
admin_role = Role.query.filter(Role.name=='admin').first()
user_role=Role_User(user_id=user.id,role_id=admin_role.id)
db.session.add(user_role)
if form.is_resource_admin.data=='is_resource_admin':
resource_admin_role = Role.query.filter(Role.name=='resource_admin').first()
user_role=Role_User(user_id=user.id,role_id=resource_admin_role.id)
db.session.add(user_role)
if form.is_generic_user.data=='is_generic_user':
generic_user_role = Role.query.filter(Role.name=='generic_user').first()
user_role=Role_User(user_id=user.id,role_id=generic_user_role.id)
db.session.add(user_role)
db.session.commit()
flash(_('You have successfully added a new user.'))
except Exception as e:
# in case user name already exists
db.session.rollback()
flash(_('User name already exists.'))
current_app.logger.exception(e)
# redirect to the user page
return redirect(url_for('auth.users_list'))
# load user template
return render_template('user_add.html',c_user=current_user,
form=form, title='Add user')