From 19d1f2b335b7c88c442adc23d694a9d6d2629b96 Mon Sep 17 00:00:00 2001 From: Haitao Yue Date: Thu, 6 Jul 2017 23:38:59 +0800 Subject: [PATCH] [CE-71] Add user authentication for react theme #close Support user login for react theme Remove auth in nginx conf Add login page for react theme Change-Id: I17ad143766ccf37a70df1d1f76905b998c0d6021 Signed-off-by: Haitao Yue --- nginx/nginx.conf | 4 +- src/config.py | 8 + src/dashboard.py | 53 +++- src/requirements.txt | 3 + src/resources/__init__.py | 2 + src/resources/auth_api.py | 80 ++++++ src/resources/index.py | 5 + src/resources/login.py | 28 +++ src/resources/models.py | 15 ++ src/resources/user.py | 81 ++++++ .../static/js/components/layout/Header.js | 2 +- src/themes/react/static/js/models/app.js | 6 +- src/themes/react/static/js/services/app.js | 4 +- src/themes/react/static/login/index.js | 24 ++ src/themes/react/static/login/models/app.js | 42 ++++ .../react/static/login/particles.min.js | 9 + src/themes/react/static/login/routes/app.js | 35 +++ src/themes/react/static/login/routes/login.js | 78 ++++++ .../react/static/login/routes/login.less | 53 ++++ .../react/static/login/routes/main.less | 4 + src/themes/react/static/login/services/app.js | 16 ++ src/themes/react/static/login/utils/config.js | 7 + src/themes/react/static/login/utils/index.js | 48 ++++ src/themes/react/static/login/utils/menu.js | 65 +++++ .../react/static/login/utils/request.js | 48 ++++ src/themes/react/static/login/utils/theme.js | 14 ++ src/themes/react/static/package.json | 3 +- src/themes/react/templates/index.html | 4 + src/themes/react/templates/login.html | 230 ++++++++++++++++++ 29 files changed, 957 insertions(+), 14 deletions(-) create mode 100644 src/resources/auth_api.py create mode 100644 src/resources/login.py create mode 100644 src/resources/models.py create mode 100644 src/resources/user.py create mode 100644 src/themes/react/static/login/index.js create mode 100644 src/themes/react/static/login/models/app.js create mode 100644 src/themes/react/static/login/particles.min.js create mode 100644 src/themes/react/static/login/routes/app.js create mode 100644 src/themes/react/static/login/routes/login.js create mode 100644 src/themes/react/static/login/routes/login.less create mode 100644 src/themes/react/static/login/routes/main.less create mode 100644 src/themes/react/static/login/services/app.js create mode 100644 src/themes/react/static/login/utils/config.js create mode 100644 src/themes/react/static/login/utils/index.js create mode 100644 src/themes/react/static/login/utils/menu.js create mode 100644 src/themes/react/static/login/utils/request.js create mode 100644 src/themes/react/static/login/utils/theme.js create mode 100644 src/themes/react/templates/login.html diff --git a/nginx/nginx.conf b/nginx/nginx.conf index bd9a7a705..e9a168322 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -82,10 +82,8 @@ http { return 444; } - auth_basic "Login"; - auth_basic_user_file /etc/nginx/.htpasswd; proxy_pass http://backend; - proxy_set_header Host $host; + proxy_set_header Host $host:8080; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Real-IP $remote_addr; } diff --git a/src/config.py b/src/config.py index 9c8d5c174..0d69318ab 100644 --- a/src/config.py +++ b/src/config.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: Apache-2.0 # +import os +import bcrypt class Config(object): @@ -16,3 +18,9 @@ class ProductionConfig(Config): class DevelopmentConfig(Config): DEBUG = True + MONGODB_DB = os.getenv('MONGODB_DB', 'dashboard') + MONGODB_HOST = os.getenv('MONGODB_HOST', 'mongo') + MONGODB_PORT = int(os.getenv('MONGODB_PORT', 27017)) + MONGODB_USERNAME = os.getenv('MONGODB_USERNAME', '') + MONGODB_PASSWORD = os.getenv('MONGODB_PASSWORD', '') + SALT = '$2b$12$e9UeM1mU0RahYaC4Ikn1Ce' diff --git a/src/dashboard.py b/src/dashboard.py index ec6c913a6..0a6e1b51f 100644 --- a/src/dashboard.py +++ b/src/dashboard.py @@ -1,15 +1,24 @@ - # Copyright IBM Corp, All Rights Reserved. # # SPDX-License-Identifier: Apache-2.0 # import os from common import log_handler, LOG_LEVEL -from flask import Flask, render_template +from flask import Flask, render_template, redirect, url_for from resources import bp_index, \ bp_stat_view, bp_stat_api, \ bp_cluster_view, bp_cluster_api, \ - bp_host_view, bp_host_api + bp_host_view, bp_host_api, bp_auth_api, bp_login +from mongoengine import connect +from flask_login import LoginManager, UserMixin, login_required +from resources.user import User +from resources import models +import bcrypt +import logging + +logger = logging.getLogger(__name__) +logger.setLevel(LOG_LEVEL) +logger.addHandler(log_handler) STATIC_FOLDER = os.getenv("STATIC_FOLDER", "themes/basic/static") TEMPLATE_FOLDER = os.getenv("TEMPLATE_FOLDER", "themes/basic/templates") @@ -19,6 +28,15 @@ app.config.from_object('config.DevelopmentConfig') app.config.from_envvar('CELLO_CONFIG_FILE', silent=True) +connect(app.config.get("MONGODB_DB", "dashboard"), + host=app.config.get("MONGODB_HOST", "mongo"), + username=app.config.get("MONGODB_USERNAME", ""), + password=app.config.get("MONGODB_PASSWORD", ""), + connect=False, tz_aware=True) + +login_manager = LoginManager() +login_manager.init_app(app) + app.logger.setLevel(LOG_LEVEL) app.logger.addHandler(log_handler) @@ -29,6 +47,18 @@ app.register_blueprint(bp_cluster_api) app.register_blueprint(bp_stat_view) app.register_blueprint(bp_stat_api) +app.register_blueprint(bp_auth_api) +app.register_blueprint(bp_login) + +admin = os.environ.get("ADMIN", "admin") +admin_password = os.environ.get("ADMIN_PASSWORD", "pass") +salt = app.config.get("SALT", b"") +password = bcrypt.hashpw(admin_password.encode('utf8'), bytes(salt.encode())) +try: + user = User(admin, password, is_admin=True) + user.save() +except Exception: + pass @app.errorhandler(404) @@ -41,6 +71,23 @@ def internal_error(error): return render_template('500.html'), 500 +@login_manager.unauthorized_handler +def unauthorized_callback(): + return redirect(url_for('bp_login.login')) + + +@login_manager.user_loader +def load_user(id): + if id is None: + redirect(url_for('bp_login.login')) + user = User() + user.get_by_id(id) + if user.is_active(): + return user + else: + return None + + if __name__ == '__main__': app.run( host='0.0.0.0', diff --git a/src/requirements.txt b/src/requirements.txt index 49e87a5ad..72bef328f 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -9,3 +9,6 @@ greenlet>=0.4.5,<=0.4.12 gunicorn>=19.0.0,<=19.6.0 pymongo>=3.2.0,<=3.4.0 requests>=2.0.0,<=2.13.0 +mongoengine +flask-login +bcrypt diff --git a/src/resources/__init__.py b/src/resources/__init__.py index bf7a35d19..6be0ce583 100644 --- a/src/resources/__init__.py +++ b/src/resources/__init__.py @@ -12,3 +12,5 @@ from .host_view import bp_host_view from .stat import bp_stat_api, bp_stat_view +from .auth_api import bp_auth_api +from .login import bp_login diff --git a/src/resources/auth_api.py b/src/resources/auth_api.py new file mode 100644 index 000000000..bdd208a9c --- /dev/null +++ b/src/resources/auth_api.py @@ -0,0 +1,80 @@ +# Copyright IBM Corp, All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +import logging +import os +import sys +import bcrypt + +from flask import Blueprint, redirect, url_for +from flask import request as r +from flask import current_app as app +from flask_login import login_user, logout_user + +sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) +from common import log_handler, LOG_LEVEL, \ + request_get, make_ok_resp, make_fail_resp, \ + request_debug, request_json_body, \ + CODE_CREATED, CODE_NOT_FOUND +from .user import User + +logger = logging.getLogger(__name__) +logger.setLevel(LOG_LEVEL) +logger.addHandler(log_handler) + +bp_auth_api = Blueprint('bp_auth_api', __name__, + url_prefix='/{}/{}'.format("api", "auth")) + + +@bp_auth_api.route('/register', methods=['POST']) +def register(): + request_debug(r, logger) + if not r.form["username"] or not r.form["password"]: + error_msg = "register without enough data" + logger.warning(error_msg) + return make_fail_resp(error=error_msg, data=r.form) + + username, password = r.form["username"], r.form["password"] + salt = app.config.get("SALT", b"") + password = bcrypt.hashpw(password.encode('utf8'), bytes(salt.encode())) + + try: + user = User(username, password) + user.save() + return make_ok_resp(code=CODE_CREATED) + except Exception as exc: + logger.info("exc %s", exc) + return make_fail_resp(error="register failed") + + +@bp_auth_api.route('/login', methods=['POST']) +def login(): + if not r.form["username"] or not r.form["password"]: + error_msg = "login without enough data" + logger.warning(error_msg) + return make_fail_resp(error=error_msg, data={'success': False}) + + username, password = r.form["username"], r.form["password"] + user_obj = User() + try: + user = user_obj.get_by_username_w_password(username) + if user.is_admin() and \ + bcrypt.checkpw(password.encode('utf8'), + bytes(user.password.encode())): + login_user(user) + return make_ok_resp(data={'success': True, + 'next': url_for('bp_index.show')}, + code=CODE_CREATED) + else: + return make_fail_resp(error="login failed", + data={'success': False}) + except Exception: + return make_fail_resp(error="login failed", data={'success': False}) + + +@bp_auth_api.route('/logout', methods=['GET']) +def logout(): + logout_user() + return make_ok_resp(data={'success': True, + 'next': url_for('bp_login.login')}) diff --git a/src/resources/index.py b/src/resources/index.py index fff9265c6..da0baba65 100644 --- a/src/resources/index.py +++ b/src/resources/index.py @@ -17,6 +17,7 @@ request_debug, \ CLUSTER_LOG_TYPES, CLUSTER_LOG_LEVEL from version import version, homepage, author +from flask_login import login_required, current_user logger = logging.getLogger(__name__) logger.setLevel(LOG_LEVEL) @@ -29,6 +30,7 @@ @bp_index.route('/', methods=['GET']) @bp_index.route('/index', methods=['GET']) +@login_required def show(): request_debug(r, logger) hosts = list(host_handler.list(filter_data={})) @@ -46,6 +48,7 @@ def show(): clusters_temp = len(list(cluster_handler.list(filter_data={ "user_id": "/^__/"}, col_name="active"))) + username, is_admin = current_user.username, current_user.isAdmin return render_template("index.html", hosts=hosts, hosts_free=hosts_free, @@ -64,6 +67,8 @@ def show(): host_types=WORKER_TYPES, log_types=CLUSTER_LOG_TYPES, log_levels=CLUSTER_LOG_LEVEL, + username=username, + is_admin=is_admin ) diff --git a/src/resources/login.py b/src/resources/login.py new file mode 100644 index 000000000..27d19928f --- /dev/null +++ b/src/resources/login.py @@ -0,0 +1,28 @@ + +# Copyright IBM Corp, All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +import logging +import os +import sys +from flask import Blueprint, render_template +from flask import request as r + +sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) +from common import log_handler, LOG_LEVEL, NETWORK_TYPES, CONSENSUS_PLUGINS, \ + CONSENSUS_MODES, WORKER_TYPES, NETWORK_SIZE_FABRIC_PRE_V1, request_debug, \ + CLUSTER_LOG_TYPES, CLUSTER_LOG_LEVEL + +logger = logging.getLogger(__name__) +logger.setLevel(LOG_LEVEL) +logger.addHandler(log_handler) + +bp_login = Blueprint('bp_login', __name__) + + +@bp_login.route('/login', methods=['GET']) +def login(): + request_debug(r, logger) + + return render_template("login.html") diff --git a/src/resources/models.py b/src/resources/models.py new file mode 100644 index 000000000..9c6a74021 --- /dev/null +++ b/src/resources/models.py @@ -0,0 +1,15 @@ +import sys +import os +import datetime +from mongoengine import Document, StringField,\ + BooleanField, DateTimeField + +sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) + + +class User(Document): + username = StringField(unique=True) + password = StringField(default=True) + active = BooleanField(default=True) + isAdmin = BooleanField(default=False) + timestamp = DateTimeField(default=datetime.datetime.now()) diff --git a/src/resources/user.py b/src/resources/user.py new file mode 100644 index 000000000..1d512eae0 --- /dev/null +++ b/src/resources/user.py @@ -0,0 +1,81 @@ +import sys +import os +import logging +sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) +from flask_login import UserMixin, AnonymousUserMixin +from resources import models +from common import log_handler, LOG_LEVEL + +logger = logging.getLogger(__name__) +logger.setLevel(LOG_LEVEL) +logger.addHandler(log_handler) + + +class User(UserMixin): + def __init__(self, username=None, password=None, active=True, + is_admin=False, id=None): + self.username = username + self.password = password + self.active = active + self.isAdmin = is_admin + self.id = None + + def is_active(self): + return self.active + + def is_admin(self): + return self.isAdmin + + def save(self): + new_user = models.User(username=self.username, + password=self.password, + active=self.active, + isAdmin=self.isAdmin) + new_user.save() + self.id = new_user.id + return self.id + + def get_by_username(self, username): + + dbUser = models.User.objects.get(username=username) + if dbUser: + self.username = dbUser.username + self.active = dbUser.active + self.id = dbUser.id + return self + else: + return None + + def get_by_username_w_password(self, username): + try: + dbUser = models.User.objects.get(username=username) + + if dbUser: + logger.info("get user") + self.username = dbUser.username + self.active = dbUser.active + self.password = dbUser.password + self.id = dbUser.id + self.isAdmin = dbUser.isAdmin + return self + else: + logger.info("not get user") + return None + except Exception as exc: + logger.info("get user exc %s", exc) + return None + + def get_by_id(self, id): + dbUser = models.User.objects.with_id(id) + if dbUser: + self.username = dbUser.username + self.active = dbUser.active + self.id = dbUser.id + + return self + else: + return None + + +class Anonymous(AnonymousUserMixin): + name = u"Anonymous" diff --git a/src/themes/react/static/js/components/layout/Header.js b/src/themes/react/static/js/components/layout/Header.js index 33c3a204c..edc86b3af 100644 --- a/src/themes/react/static/js/components/layout/Header.js +++ b/src/themes/react/static/js/components/layout/Header.js @@ -38,7 +38,7 @@ function Header ({ user, logout, switchSider, siderFold, isNavbar, menuPopoverVi - {user.name} } + {window.username} } > logout diff --git a/src/themes/react/static/js/models/app.js b/src/themes/react/static/js/models/app.js index 62ea16667..768160fb0 100644 --- a/src/themes/react/static/js/models/app.js +++ b/src/themes/react/static/js/models/app.js @@ -67,10 +67,8 @@ export default { payload, }, { call, put }) { const data = yield call(logout, parse(payload)) - if (data.success) { - yield put({ - type: 'logoutSuccess', - }) + if (data && data.data.success) { + window.location.href = data.data.next; } }, *switchSider ({ diff --git a/src/themes/react/static/js/services/app.js b/src/themes/react/static/js/services/app.js index 7944ec6a4..17a41fffe 100644 --- a/src/themes/react/static/js/services/app.js +++ b/src/themes/react/static/js/services/app.js @@ -16,8 +16,8 @@ export async function login (params) { export async function logout (params) { return request({ - url: '/api/logout', - method: 'post', + url: '/api/auth/logout', + method: 'get', data: params, }) } diff --git a/src/themes/react/static/login/index.js b/src/themes/react/static/login/index.js new file mode 100644 index 000000000..c1f3c7314 --- /dev/null +++ b/src/themes/react/static/login/index.js @@ -0,0 +1,24 @@ +import 'babel-polyfill' +import dva from 'dva' +import createLoading from 'dva-loading' +import { browserHistory } from 'dva/router' +import App from './routes/app' + +// 1. Initialize +const app = dva({ + ...createLoading(), + history: browserHistory, + onError (error) { + console.error('app onError -- ', error) + }, +}) + +// 2. Model +app.model(require('./models/app')) + +// 3. Router +// app.router(require('./router')) +app.router(() => ) + +// 4. Start +app.start('#root') diff --git a/src/themes/react/static/login/models/app.js b/src/themes/react/static/login/models/app.js new file mode 100644 index 000000000..92202fc87 --- /dev/null +++ b/src/themes/react/static/login/models/app.js @@ -0,0 +1,42 @@ +import { login } from '../services/app' +import { parse } from 'qs' + +export default { + namespace: 'app', + state: { + logging: false, + loginFail: false + }, + subscriptions: { + setup ({ dispatch }) { + }, + }, + effects: { + *login({payload}, {call, put}) { + yield put({type: 'showLogging'}) + try { + const data = yield call(login, payload) + if (data && data.data.success) { + window.location.href = data.data.next; + } else { + yield put({type: 'setLoginFail'}) + yield put({type: 'hideLogging'}) + } + } catch (e) { + yield put({type: 'setLoginFail'}) + yield put({type: 'hideLogging'}) + } + } + }, + reducers: { + showLogging(state) { + return {...state, logging: true, loginFail: false} + }, + hideLogging(state) { + return {...state, logging: false} + }, + setLoginFail(state) { + return {...state, loginFail: true} + } + }, +} diff --git a/src/themes/react/static/login/particles.min.js b/src/themes/react/static/login/particles.min.js new file mode 100644 index 000000000..b3d46d127 --- /dev/null +++ b/src/themes/react/static/login/particles.min.js @@ -0,0 +1,9 @@ +/* ----------------------------------------------- +/* Author : Vincent Garreau - vincentgarreau.com +/* MIT license: http://opensource.org/licenses/MIT +/* Demo / Generator : vincentgarreau.com/particles.js +/* GitHub : github.com/VincentGarreau/particles.js +/* How to use? : Check the GitHub README +/* v2.0.0 +/* ----------------------------------------------- */ +function hexToRgb(e){var a=/^#?([a-f\d])([a-f\d])([a-f\d])$/i;e=e.replace(a,function(e,a,t,i){return a+a+t+t+i+i});var t=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(e);return t?{r:parseInt(t[1],16),g:parseInt(t[2],16),b:parseInt(t[3],16)}:null}function clamp(e,a,t){return Math.min(Math.max(e,a),t)}function isInArray(e,a){return a.indexOf(e)>-1}var pJS=function(e,a){var t=document.querySelector("#"+e+" > .particles-js-canvas-el");this.pJS={canvas:{el:t,w:t.offsetWidth,h:t.offsetHeight},particles:{number:{value:400,density:{enable:!0,value_area:800}},color:{value:"#fff"},shape:{type:"circle",stroke:{width:0,color:"#ff0000"},polygon:{nb_sides:5},image:{src:"",width:100,height:100}},opacity:{value:1,random:!1,anim:{enable:!1,speed:2,opacity_min:0,sync:!1}},size:{value:20,random:!1,anim:{enable:!1,speed:20,size_min:0,sync:!1}},line_linked:{enable:!0,distance:100,color:"#fff",opacity:1,width:1},move:{enable:!0,speed:2,direction:"none",random:!1,straight:!1,out_mode:"out",bounce:!1,attract:{enable:!1,rotateX:3e3,rotateY:3e3}},array:[]},interactivity:{detect_on:"canvas",events:{onhover:{enable:!0,mode:"grab"},onclick:{enable:!0,mode:"push"},resize:!0},modes:{grab:{distance:100,line_linked:{opacity:1}},bubble:{distance:200,size:80,duration:.4},repulse:{distance:200,duration:.4},push:{particles_nb:4},remove:{particles_nb:2}},mouse:{}},retina_detect:!1,fn:{interact:{},modes:{},vendors:{}},tmp:{}};var i=this.pJS;a&&Object.deepExtend(i,a),i.tmp.obj={size_value:i.particles.size.value,size_anim_speed:i.particles.size.anim.speed,move_speed:i.particles.move.speed,line_linked_distance:i.particles.line_linked.distance,line_linked_width:i.particles.line_linked.width,mode_grab_distance:i.interactivity.modes.grab.distance,mode_bubble_distance:i.interactivity.modes.bubble.distance,mode_bubble_size:i.interactivity.modes.bubble.size,mode_repulse_distance:i.interactivity.modes.repulse.distance},i.fn.retinaInit=function(){i.retina_detect&&window.devicePixelRatio>1?(i.canvas.pxratio=window.devicePixelRatio,i.tmp.retina=!0):(i.canvas.pxratio=1,i.tmp.retina=!1),i.canvas.w=i.canvas.el.offsetWidth*i.canvas.pxratio,i.canvas.h=i.canvas.el.offsetHeight*i.canvas.pxratio,i.particles.size.value=i.tmp.obj.size_value*i.canvas.pxratio,i.particles.size.anim.speed=i.tmp.obj.size_anim_speed*i.canvas.pxratio,i.particles.move.speed=i.tmp.obj.move_speed*i.canvas.pxratio,i.particles.line_linked.distance=i.tmp.obj.line_linked_distance*i.canvas.pxratio,i.interactivity.modes.grab.distance=i.tmp.obj.mode_grab_distance*i.canvas.pxratio,i.interactivity.modes.bubble.distance=i.tmp.obj.mode_bubble_distance*i.canvas.pxratio,i.particles.line_linked.width=i.tmp.obj.line_linked_width*i.canvas.pxratio,i.interactivity.modes.bubble.size=i.tmp.obj.mode_bubble_size*i.canvas.pxratio,i.interactivity.modes.repulse.distance=i.tmp.obj.mode_repulse_distance*i.canvas.pxratio},i.fn.canvasInit=function(){i.canvas.ctx=i.canvas.el.getContext("2d")},i.fn.canvasSize=function(){i.canvas.el.width=i.canvas.w,i.canvas.el.height=i.canvas.h,i&&i.interactivity.events.resize&&window.addEventListener("resize",function(){i.canvas.w=i.canvas.el.offsetWidth,i.canvas.h=i.canvas.el.offsetHeight,i.tmp.retina&&(i.canvas.w*=i.canvas.pxratio,i.canvas.h*=i.canvas.pxratio),i.canvas.el.width=i.canvas.w,i.canvas.el.height=i.canvas.h,i.particles.move.enable||(i.fn.particlesEmpty(),i.fn.particlesCreate(),i.fn.particlesDraw(),i.fn.vendors.densityAutoParticles()),i.fn.vendors.densityAutoParticles()})},i.fn.canvasPaint=function(){i.canvas.ctx.fillRect(0,0,i.canvas.w,i.canvas.h)},i.fn.canvasClear=function(){i.canvas.ctx.clearRect(0,0,i.canvas.w,i.canvas.h)},i.fn.particle=function(e,a,t){if(this.radius=(i.particles.size.random?Math.random():1)*i.particles.size.value,i.particles.size.anim.enable&&(this.size_status=!1,this.vs=i.particles.size.anim.speed/100,i.particles.size.anim.sync||(this.vs=this.vs*Math.random())),this.x=t?t.x:Math.random()*i.canvas.w,this.y=t?t.y:Math.random()*i.canvas.h,this.x>i.canvas.w-2*this.radius?this.x=this.x-this.radius:this.x<2*this.radius&&(this.x=this.x+this.radius),this.y>i.canvas.h-2*this.radius?this.y=this.y-this.radius:this.y<2*this.radius&&(this.y=this.y+this.radius),i.particles.move.bounce&&i.fn.vendors.checkOverlap(this,t),this.color={},"object"==typeof e.value)if(e.value instanceof Array){var s=e.value[Math.floor(Math.random()*i.particles.color.value.length)];this.color.rgb=hexToRgb(s)}else void 0!=e.value.r&&void 0!=e.value.g&&void 0!=e.value.b&&(this.color.rgb={r:e.value.r,g:e.value.g,b:e.value.b}),void 0!=e.value.h&&void 0!=e.value.s&&void 0!=e.value.l&&(this.color.hsl={h:e.value.h,s:e.value.s,l:e.value.l});else"random"==e.value?this.color.rgb={r:Math.floor(256*Math.random())+0,g:Math.floor(256*Math.random())+0,b:Math.floor(256*Math.random())+0}:"string"==typeof e.value&&(this.color=e,this.color.rgb=hexToRgb(this.color.value));this.opacity=(i.particles.opacity.random?Math.random():1)*i.particles.opacity.value,i.particles.opacity.anim.enable&&(this.opacity_status=!1,this.vo=i.particles.opacity.anim.speed/100,i.particles.opacity.anim.sync||(this.vo=this.vo*Math.random()));var n={};switch(i.particles.move.direction){case"top":n={x:0,y:-1};break;case"top-right":n={x:.5,y:-.5};break;case"right":n={x:1,y:-0};break;case"bottom-right":n={x:.5,y:.5};break;case"bottom":n={x:0,y:1};break;case"bottom-left":n={x:-.5,y:1};break;case"left":n={x:-1,y:0};break;case"top-left":n={x:-.5,y:-.5};break;default:n={x:0,y:0}}i.particles.move.straight?(this.vx=n.x,this.vy=n.y,i.particles.move.random&&(this.vx=this.vx*Math.random(),this.vy=this.vy*Math.random())):(this.vx=n.x+Math.random()-.5,this.vy=n.y+Math.random()-.5),this.vx_i=this.vx,this.vy_i=this.vy;var r=i.particles.shape.type;if("object"==typeof r){if(r instanceof Array){var c=r[Math.floor(Math.random()*r.length)];this.shape=c}}else this.shape=r;if("image"==this.shape){var o=i.particles.shape;this.img={src:o.image.src,ratio:o.image.width/o.image.height},this.img.ratio||(this.img.ratio=1),"svg"==i.tmp.img_type&&void 0!=i.tmp.source_svg&&(i.fn.vendors.createSvgImg(this),i.tmp.pushing&&(this.img.loaded=!1))}},i.fn.particle.prototype.draw=function(){function e(){i.canvas.ctx.drawImage(r,a.x-t,a.y-t,2*t,2*t/a.img.ratio)}var a=this;if(void 0!=a.radius_bubble)var t=a.radius_bubble;else var t=a.radius;if(void 0!=a.opacity_bubble)var s=a.opacity_bubble;else var s=a.opacity;if(a.color.rgb)var n="rgba("+a.color.rgb.r+","+a.color.rgb.g+","+a.color.rgb.b+","+s+")";else var n="hsla("+a.color.hsl.h+","+a.color.hsl.s+"%,"+a.color.hsl.l+"%,"+s+")";switch(i.canvas.ctx.fillStyle=n,i.canvas.ctx.beginPath(),a.shape){case"circle":i.canvas.ctx.arc(a.x,a.y,t,0,2*Math.PI,!1);break;case"edge":i.canvas.ctx.rect(a.x-t,a.y-t,2*t,2*t);break;case"triangle":i.fn.vendors.drawShape(i.canvas.ctx,a.x-t,a.y+t/1.66,2*t,3,2);break;case"polygon":i.fn.vendors.drawShape(i.canvas.ctx,a.x-t/(i.particles.shape.polygon.nb_sides/3.5),a.y-t/.76,2.66*t/(i.particles.shape.polygon.nb_sides/3),i.particles.shape.polygon.nb_sides,1);break;case"star":i.fn.vendors.drawShape(i.canvas.ctx,a.x-2*t/(i.particles.shape.polygon.nb_sides/4),a.y-t/1.52,2*t*2.66/(i.particles.shape.polygon.nb_sides/3),i.particles.shape.polygon.nb_sides,2);break;case"image":if("svg"==i.tmp.img_type)var r=a.img.obj;else var r=i.tmp.img_obj;r&&e()}i.canvas.ctx.closePath(),i.particles.shape.stroke.width>0&&(i.canvas.ctx.strokeStyle=i.particles.shape.stroke.color,i.canvas.ctx.lineWidth=i.particles.shape.stroke.width,i.canvas.ctx.stroke()),i.canvas.ctx.fill()},i.fn.particlesCreate=function(){for(var e=0;e=i.particles.opacity.value&&(a.opacity_status=!1),a.opacity+=a.vo):(a.opacity<=i.particles.opacity.anim.opacity_min&&(a.opacity_status=!0),a.opacity-=a.vo),a.opacity<0&&(a.opacity=0)),i.particles.size.anim.enable&&(1==a.size_status?(a.radius>=i.particles.size.value&&(a.size_status=!1),a.radius+=a.vs):(a.radius<=i.particles.size.anim.size_min&&(a.size_status=!0),a.radius-=a.vs),a.radius<0&&(a.radius=0)),"bounce"==i.particles.move.out_mode)var s={x_left:a.radius,x_right:i.canvas.w,y_top:a.radius,y_bottom:i.canvas.h};else var s={x_left:-a.radius,x_right:i.canvas.w+a.radius,y_top:-a.radius,y_bottom:i.canvas.h+a.radius};switch(a.x-a.radius>i.canvas.w?(a.x=s.x_left,a.y=Math.random()*i.canvas.h):a.x+a.radius<0&&(a.x=s.x_right,a.y=Math.random()*i.canvas.h),a.y-a.radius>i.canvas.h?(a.y=s.y_top,a.x=Math.random()*i.canvas.w):a.y+a.radius<0&&(a.y=s.y_bottom,a.x=Math.random()*i.canvas.w),i.particles.move.out_mode){case"bounce":a.x+a.radius>i.canvas.w?a.vx=-a.vx:a.x-a.radius<0&&(a.vx=-a.vx),a.y+a.radius>i.canvas.h?a.vy=-a.vy:a.y-a.radius<0&&(a.vy=-a.vy)}if(isInArray("grab",i.interactivity.events.onhover.mode)&&i.fn.modes.grabParticle(a),(isInArray("bubble",i.interactivity.events.onhover.mode)||isInArray("bubble",i.interactivity.events.onclick.mode))&&i.fn.modes.bubbleParticle(a),(isInArray("repulse",i.interactivity.events.onhover.mode)||isInArray("repulse",i.interactivity.events.onclick.mode))&&i.fn.modes.repulseParticle(a),i.particles.line_linked.enable||i.particles.move.attract.enable)for(var n=e+1;n0){var c=i.particles.line_linked.color_rgb_line;i.canvas.ctx.strokeStyle="rgba("+c.r+","+c.g+","+c.b+","+r+")",i.canvas.ctx.lineWidth=i.particles.line_linked.width,i.canvas.ctx.beginPath(),i.canvas.ctx.moveTo(e.x,e.y),i.canvas.ctx.lineTo(a.x,a.y),i.canvas.ctx.stroke(),i.canvas.ctx.closePath()}}},i.fn.interact.attractParticles=function(e,a){var t=e.x-a.x,s=e.y-a.y,n=Math.sqrt(t*t+s*s);if(n<=i.particles.line_linked.distance){var r=t/(1e3*i.particles.move.attract.rotateX),c=s/(1e3*i.particles.move.attract.rotateY);e.vx-=r,e.vy-=c,a.vx+=r,a.vy+=c}},i.fn.interact.bounceParticles=function(e,a){var t=e.x-a.x,i=e.y-a.y,s=Math.sqrt(t*t+i*i),n=e.radius+a.radius;n>=s&&(e.vx=-e.vx,e.vy=-e.vy,a.vx=-a.vx,a.vy=-a.vy)},i.fn.modes.pushParticles=function(e,a){i.tmp.pushing=!0;for(var t=0;e>t;t++)i.particles.array.push(new i.fn.particle(i.particles.color,i.particles.opacity.value,{x:a?a.pos_x:Math.random()*i.canvas.w,y:a?a.pos_y:Math.random()*i.canvas.h})),t==e-1&&(i.particles.move.enable||i.fn.particlesDraw(),i.tmp.pushing=!1)},i.fn.modes.removeParticles=function(e){i.particles.array.splice(0,e),i.particles.move.enable||i.fn.particlesDraw()},i.fn.modes.bubbleParticle=function(e){function a(){e.opacity_bubble=e.opacity,e.radius_bubble=e.radius}function t(a,t,s,n,c){if(a!=t)if(i.tmp.bubble_duration_end){if(void 0!=s){var o=n-p*(n-a)/i.interactivity.modes.bubble.duration,l=a-o;d=a+l,"size"==c&&(e.radius_bubble=d),"opacity"==c&&(e.opacity_bubble=d)}}else if(r<=i.interactivity.modes.bubble.distance){if(void 0!=s)var v=s;else var v=n;if(v!=a){var d=n-p*(n-a)/i.interactivity.modes.bubble.duration;"size"==c&&(e.radius_bubble=d),"opacity"==c&&(e.opacity_bubble=d)}}else"size"==c&&(e.radius_bubble=void 0),"opacity"==c&&(e.opacity_bubble=void 0)}if(i.interactivity.events.onhover.enable&&isInArray("bubble",i.interactivity.events.onhover.mode)){var s=e.x-i.interactivity.mouse.pos_x,n=e.y-i.interactivity.mouse.pos_y,r=Math.sqrt(s*s+n*n),c=1-r/i.interactivity.modes.bubble.distance;if(r<=i.interactivity.modes.bubble.distance){if(c>=0&&"mousemove"==i.interactivity.status){if(i.interactivity.modes.bubble.size!=i.particles.size.value)if(i.interactivity.modes.bubble.size>i.particles.size.value){var o=e.radius+i.interactivity.modes.bubble.size*c;o>=0&&(e.radius_bubble=o)}else{var l=e.radius-i.interactivity.modes.bubble.size,o=e.radius-l*c;o>0?e.radius_bubble=o:e.radius_bubble=0}if(i.interactivity.modes.bubble.opacity!=i.particles.opacity.value)if(i.interactivity.modes.bubble.opacity>i.particles.opacity.value){var v=i.interactivity.modes.bubble.opacity*c;v>e.opacity&&v<=i.interactivity.modes.bubble.opacity&&(e.opacity_bubble=v)}else{var v=e.opacity-(i.particles.opacity.value-i.interactivity.modes.bubble.opacity)*c;v=i.interactivity.modes.bubble.opacity&&(e.opacity_bubble=v)}}}else a();"mouseleave"==i.interactivity.status&&a()}else if(i.interactivity.events.onclick.enable&&isInArray("bubble",i.interactivity.events.onclick.mode)){if(i.tmp.bubble_clicking){var s=e.x-i.interactivity.mouse.click_pos_x,n=e.y-i.interactivity.mouse.click_pos_y,r=Math.sqrt(s*s+n*n),p=((new Date).getTime()-i.interactivity.mouse.click_time)/1e3;p>i.interactivity.modes.bubble.duration&&(i.tmp.bubble_duration_end=!0),p>2*i.interactivity.modes.bubble.duration&&(i.tmp.bubble_clicking=!1,i.tmp.bubble_duration_end=!1)}i.tmp.bubble_clicking&&(t(i.interactivity.modes.bubble.size,i.particles.size.value,e.radius_bubble,e.radius,"size"),t(i.interactivity.modes.bubble.opacity,i.particles.opacity.value,e.opacity_bubble,e.opacity,"opacity"))}},i.fn.modes.repulseParticle=function(e){function a(){var a=Math.atan2(d,p);if(e.vx=u*Math.cos(a),e.vy=u*Math.sin(a),"bounce"==i.particles.move.out_mode){var t={x:e.x+e.vx,y:e.y+e.vy};t.x+e.radius>i.canvas.w?e.vx=-e.vx:t.x-e.radius<0&&(e.vx=-e.vx),t.y+e.radius>i.canvas.h?e.vy=-e.vy:t.y-e.radius<0&&(e.vy=-e.vy)}}if(i.interactivity.events.onhover.enable&&isInArray("repulse",i.interactivity.events.onhover.mode)&&"mousemove"==i.interactivity.status){var t=e.x-i.interactivity.mouse.pos_x,s=e.y-i.interactivity.mouse.pos_y,n=Math.sqrt(t*t+s*s),r={x:t/n,y:s/n},c=i.interactivity.modes.repulse.distance,o=100,l=clamp(1/c*(-1*Math.pow(n/c,2)+1)*c*o,0,50),v={x:e.x+r.x*l,y:e.y+r.y*l};"bounce"==i.particles.move.out_mode?(v.x-e.radius>0&&v.x+e.radius0&&v.y+e.radius=m&&a()}else 0==i.tmp.repulse_clicking&&(e.vx=e.vx_i,e.vy=e.vy_i)},i.fn.modes.grabParticle=function(e){if(i.interactivity.events.onhover.enable&&"mousemove"==i.interactivity.status){var a=e.x-i.interactivity.mouse.pos_x,t=e.y-i.interactivity.mouse.pos_y,s=Math.sqrt(a*a+t*t);if(s<=i.interactivity.modes.grab.distance){var n=i.interactivity.modes.grab.line_linked.opacity-s/(1/i.interactivity.modes.grab.line_linked.opacity)/i.interactivity.modes.grab.distance;if(n>0){var r=i.particles.line_linked.color_rgb_line;i.canvas.ctx.strokeStyle="rgba("+r.r+","+r.g+","+r.b+","+n+")",i.canvas.ctx.lineWidth=i.particles.line_linked.width,i.canvas.ctx.beginPath(),i.canvas.ctx.moveTo(e.x,e.y),i.canvas.ctx.lineTo(i.interactivity.mouse.pos_x,i.interactivity.mouse.pos_y),i.canvas.ctx.stroke(),i.canvas.ctx.closePath()}}}},i.fn.vendors.eventsListeners=function(){"window"==i.interactivity.detect_on?i.interactivity.el=window:i.interactivity.el=i.canvas.el,(i.interactivity.events.onhover.enable||i.interactivity.events.onclick.enable)&&(i.interactivity.el.addEventListener("mousemove",function(e){if(i.interactivity.el==window)var a=e.clientX,t=e.clientY;else var a=e.offsetX||e.clientX,t=e.offsetY||e.clientY;i.interactivity.mouse.pos_x=a,i.interactivity.mouse.pos_y=t,i.tmp.retina&&(i.interactivity.mouse.pos_x*=i.canvas.pxratio,i.interactivity.mouse.pos_y*=i.canvas.pxratio),i.interactivity.status="mousemove"}),i.interactivity.el.addEventListener("mouseleave",function(e){i.interactivity.mouse.pos_x=null,i.interactivity.mouse.pos_y=null,i.interactivity.status="mouseleave"})),i.interactivity.events.onclick.enable&&i.interactivity.el.addEventListener("click",function(){if(i.interactivity.mouse.click_pos_x=i.interactivity.mouse.pos_x,i.interactivity.mouse.click_pos_y=i.interactivity.mouse.pos_y,i.interactivity.mouse.click_time=(new Date).getTime(),i.interactivity.events.onclick.enable)switch(i.interactivity.events.onclick.mode){case"push":i.particles.move.enable?i.fn.modes.pushParticles(i.interactivity.modes.push.particles_nb,i.interactivity.mouse):1==i.interactivity.modes.push.particles_nb?i.fn.modes.pushParticles(i.interactivity.modes.push.particles_nb,i.interactivity.mouse):i.interactivity.modes.push.particles_nb>1&&i.fn.modes.pushParticles(i.interactivity.modes.push.particles_nb);break;case"remove":i.fn.modes.removeParticles(i.interactivity.modes.remove.particles_nb);break;case"bubble":i.tmp.bubble_clicking=!0;break;case"repulse":i.tmp.repulse_clicking=!0,i.tmp.repulse_count=0,i.tmp.repulse_finish=!1,setTimeout(function(){i.tmp.repulse_clicking=!1},1e3*i.interactivity.modes.repulse.duration)}})},i.fn.vendors.densityAutoParticles=function(){if(i.particles.number.density.enable){var e=i.canvas.el.width*i.canvas.el.height/1e3;i.tmp.retina&&(e/=2*i.canvas.pxratio);var a=e*i.particles.number.value/i.particles.number.density.value_area,t=i.particles.array.length-a;0>t?i.fn.modes.pushParticles(Math.abs(t)):i.fn.modes.removeParticles(t)}},i.fn.vendors.checkOverlap=function(e,a){for(var t=0;tv;v++)e.lineTo(i,0),e.translate(i,0),e.rotate(l);e.fill(),e.restore()},i.fn.vendors.exportImg=function(){window.open(i.canvas.el.toDataURL("image/png"),"_blank")},i.fn.vendors.loadImg=function(e){if(i.tmp.img_error=void 0,""!=i.particles.shape.image.src)if("svg"==e){var a=new XMLHttpRequest;a.open("GET",i.particles.shape.image.src),a.onreadystatechange=function(e){4==a.readyState&&(200==a.status?(i.tmp.source_svg=e.currentTarget.response,i.fn.vendors.checkBeforeDraw()):(console.log("Error pJS - Image not found"),i.tmp.img_error=!0))},a.send()}else{var t=new Image;t.addEventListener("load",function(){i.tmp.img_obj=t,i.fn.vendors.checkBeforeDraw()}),t.src=i.particles.shape.image.src}else console.log("Error pJS - No image.src"),i.tmp.img_error=!0},i.fn.vendors.draw=function(){"image"==i.particles.shape.type?"svg"==i.tmp.img_type?i.tmp.count_svg>=i.particles.number.value?(i.fn.particlesDraw(),i.particles.move.enable?i.fn.drawAnimFrame=requestAnimFrame(i.fn.vendors.draw):cancelRequestAnimFrame(i.fn.drawAnimFrame)):i.tmp.img_error||(i.fn.drawAnimFrame=requestAnimFrame(i.fn.vendors.draw)):void 0!=i.tmp.img_obj?(i.fn.particlesDraw(),i.particles.move.enable?i.fn.drawAnimFrame=requestAnimFrame(i.fn.vendors.draw):cancelRequestAnimFrame(i.fn.drawAnimFrame)):i.tmp.img_error||(i.fn.drawAnimFrame=requestAnimFrame(i.fn.vendors.draw)):(i.fn.particlesDraw(),i.particles.move.enable?i.fn.drawAnimFrame=requestAnimFrame(i.fn.vendors.draw):cancelRequestAnimFrame(i.fn.drawAnimFrame))},i.fn.vendors.checkBeforeDraw=function(){"image"==i.particles.shape.type?"svg"==i.tmp.img_type&&void 0==i.tmp.source_svg?i.tmp.checkAnimFrame=requestAnimFrame(check):(cancelRequestAnimFrame(i.tmp.checkAnimFrame),i.tmp.img_error||(i.fn.vendors.init(),i.fn.vendors.draw())):(i.fn.vendors.init(),i.fn.vendors.draw())},i.fn.vendors.init=function(){i.fn.retinaInit(),i.fn.canvasInit(),i.fn.canvasSize(),i.fn.canvasPaint(),i.fn.particlesCreate(),i.fn.vendors.densityAutoParticles(),i.particles.line_linked.color_rgb_line=hexToRgb(i.particles.line_linked.color)},i.fn.vendors.start=function(){isInArray("image",i.particles.shape.type)?(i.tmp.img_type=i.particles.shape.image.src.substr(i.particles.shape.image.src.length-3),i.fn.vendors.loadImg(i.tmp.img_type)):i.fn.vendors.checkBeforeDraw()},i.fn.vendors.eventsListeners(),i.fn.vendors.start()};Object.deepExtend=function(e,a){for(var t in a)a[t]&&a[t].constructor&&a[t].constructor===Object?(e[t]=e[t]||{},arguments.callee(e[t],a[t])):e[t]=a[t];return e},window.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(e){window.setTimeout(e,1e3/60)}}(),window.cancelRequestAnimFrame=function(){return window.cancelAnimationFrame||window.webkitCancelRequestAnimationFrame||window.mozCancelRequestAnimationFrame||window.oCancelRequestAnimationFrame||window.msCancelRequestAnimationFrame||clearTimeout}(),window.pJSDom=[],window.particlesJS=function(e,a){"string"!=typeof e&&(a=e,e="particles-js"),e||(e="particles-js");var t=document.getElementById(e),i="particles-js-canvas-el",s=t.getElementsByClassName(i);if(s.length)for(;s.length>0;)t.removeChild(s[0]);var n=document.createElement("canvas");n.className=i,n.style.width="100%",n.style.height="100%";var r=document.getElementById(e).appendChild(n);null!=r&&pJSDom.push(new pJS(e,a))},window.particlesJS.load=function(e,a,t){var i=new XMLHttpRequest;i.open("GET",a),i.onreadystatechange=function(a){if(4==i.readyState)if(200==i.status){var s=JSON.parse(a.currentTarget.response);window.particlesJS(e,s),t&&t()}else console.log("Error pJS - XMLHttpRequest status: "+i.status),console.log("Error pJS - File config not found")},i.send()}; \ No newline at end of file diff --git a/src/themes/react/static/login/routes/app.js b/src/themes/react/static/login/routes/app.js new file mode 100644 index 000000000..623567b60 --- /dev/null +++ b/src/themes/react/static/login/routes/app.js @@ -0,0 +1,35 @@ +import React, { PropTypes } from 'react' +import { connect } from 'dva' +import Login from './login' +import { Spin } from 'antd' +import { classnames } from '../utils' +import styles from './main.less' + +function App ({ children, location, dispatch, app, loading }) { + const { login, loginButtonLoading, logging, loginFail } = app + const loginProps = { + logging, + loginFail, + loading, + loginButtonLoading, + onOk (data) { + dispatch({ type: 'app/login', payload: data }) + }, + } + + return ( +
+ +
+ ) +} + +App.propTypes = { + children: PropTypes.element.isRequired, + location: PropTypes.object, + dispatch: PropTypes.func, + app: PropTypes.object, + loading: PropTypes.bool, +} + +export default connect(({ app, loading }) => ({ app, loading: loading.models.app }))(App) diff --git a/src/themes/react/static/login/routes/login.js b/src/themes/react/static/login/routes/login.js new file mode 100644 index 000000000..d8b6009dd --- /dev/null +++ b/src/themes/react/static/login/routes/login.js @@ -0,0 +1,78 @@ +import React, { PropTypes } from 'react' +import { Button, Row, Col, Form, Input } from 'antd' +import { config } from '../utils' +import styles from './login.less' + +const FormItem = Form.Item + +const login = ({ + logging, + loginButtonLoading, + loginFail, + onOk, + form: { + getFieldDecorator, + validateFieldsAndScroll, + }, +}) => { + function handleOk () { + validateFieldsAndScroll((errors, values) => { + if (errors) { + return + } + onOk(values) + }) + } + + return ( +
+
+ + CELLO + +
+
+ + {getFieldDecorator('username', { + rules: [ + { + required: true, + message: 'Please input username', + }, + ], + })()} + + + {getFieldDecorator('password', { + rules: [ + { + required: true, + message: 'Please input password', + }, + ], + })()} + + + {loginFail && + + Validation failed + + } + + + + +
+
+ ) +} + +login.propTypes = { + form: PropTypes.object, + loginButtonLoading: PropTypes.bool, + onOk: PropTypes.func, +} + +export default Form.create()(login) diff --git a/src/themes/react/static/login/routes/login.less b/src/themes/react/static/login/routes/login.less new file mode 100644 index 000000000..5b89ecd81 --- /dev/null +++ b/src/themes/react/static/login/routes/login.less @@ -0,0 +1,53 @@ +.form { + position: absolute; + top: 50%; + left: 70%; + margin: -160px 0 0 -160px; + width: 350px; + height: 350px; + padding: 36px; + box-shadow: 0 0 100px rgba(0,0,0,.08); + background-color: rgba(255,255,255,0.7); + + button { + width: 100%; + } + + p { + color: rgb(204, 204, 204); + text-align: center; + margin-top: 16px; + + span { + &:first-child { + margin-right: 16px; + } + } + } +} + +.logo { + text-align: center; + height: 40px; + line-height: 40px; + cursor: pointer; + margin-bottom: 24px; + + img { + width: 60px; + margin-right: 8px; + } + + span { + vertical-align: text-bottom; + font-size: 16px; + text-transform: uppercase; + display: inline-block; + color: #595f69; + } +} + +.ant-spin-container, +.ant-spin-nested-loading { + height: 100%; +} diff --git a/src/themes/react/static/login/routes/main.less b/src/themes/react/static/login/routes/main.less new file mode 100644 index 000000000..60f3399bc --- /dev/null +++ b/src/themes/react/static/login/routes/main.less @@ -0,0 +1,4 @@ +.login { + background-image: url("/notebook/static/custom/img/login-bg.jpg"); + height: 100vh; +} \ No newline at end of file diff --git a/src/themes/react/static/login/services/app.js b/src/themes/react/static/login/services/app.js new file mode 100644 index 000000000..9a25fc395 --- /dev/null +++ b/src/themes/react/static/login/services/app.js @@ -0,0 +1,16 @@ +import { config, request } from '../utils' + +export async function login (params) { + return request({ + url: config.urls.login, + method: 'post', + data: params + }) +} + +export async function logout (params) { + return request('/api/logout', { + method: 'post', + data: params, + }) +} diff --git a/src/themes/react/static/login/utils/config.js b/src/themes/react/static/login/utils/config.js new file mode 100644 index 000000000..cfc7a3c93 --- /dev/null +++ b/src/themes/react/static/login/utils/config.js @@ -0,0 +1,7 @@ +module.exports = { + logoSrc: '/static/logo.svg', + logoText: 'CELLO', + urls: { + login: '/api/auth/login' + } +} diff --git a/src/themes/react/static/login/utils/index.js b/src/themes/react/static/login/utils/index.js new file mode 100644 index 000000000..3ba04f1c7 --- /dev/null +++ b/src/themes/react/static/login/utils/index.js @@ -0,0 +1,48 @@ +import config from './config' +import menu from './menu' +import request from './request' +import classnames from 'classnames' +import { color } from './theme' + +// 连字符转驼峰 +String.prototype.hyphenToHump = function () { + return this.replace(/-(\w)/g, (...args) => { + return args[1].toUpperCase() + }) +} + +// 驼峰转连字符 +String.prototype.humpToHyphen = function () { + return this.replace(/([A-Z])/g, '-$1').toLowerCase() +} + +// 日期格式化 +Date.prototype.format = function (format) { + const o = { + 'M+': this.getMonth() + 1, + 'd+': this.getDate(), + 'h+': this.getHours(), + 'H+': this.getHours(), + 'm+': this.getMinutes(), + 's+': this.getSeconds(), + 'q+': Math.floor((this.getMonth() + 3) / 3), + S: this.getMilliseconds(), + } + if (/(y+)/.test(format)) { + format = format.replace(RegExp.$1, `${this.getFullYear()}`.substr(4 - RegExp.$1.length)) + } + for (let k in o) { + if (new RegExp(`(${k})`).test(format)) { + format = format.replace(RegExp.$1, RegExp.$1.length === 1 ? o[k] : (`00${o[k]}`).substr(`${o[k]}`.length)) + } + } + return format +} + +module.exports = { + config, + menu, + request, + color, + classnames, +} diff --git a/src/themes/react/static/login/utils/menu.js b/src/themes/react/static/login/utils/menu.js new file mode 100644 index 000000000..6d12eaf4d --- /dev/null +++ b/src/themes/react/static/login/utils/menu.js @@ -0,0 +1,65 @@ +module.exports = [ + { + key: 'dashboard', + name: '仪表盘', + icon: 'laptop', + }, + { + key: 'users', + name: '用户管理', + icon: 'user', + }, + { + key: 'ui', + name: 'UI组件', + icon: 'camera-o', + clickable: false, + child: [ + { + key: 'ico', + name: 'Ico 图标', + }, + { + key: 'search', + name: 'Search 搜索', + }, + { + key: 'dropOption', + name: 'DropOption 下拉操作', + }, + { + key: 'layer', + name: 'Layer 弹层', + }, + { + key: 'DataTable', + name: 'DataTable 数据表格', + }, + ], + }, + { + key: 'navigation', + name: '测试导航', + icon: 'setting', + child: [ + { + key: 'navigation1', + name: '二级导航1', + }, + { + key: 'navigation2', + name: '二级导航2', + child: [ + { + key: 'navigation21', + name: '三级导航1', + }, + { + key: 'navigation22', + name: '三级导航2', + }, + ], + }, + ], + }, +] diff --git a/src/themes/react/static/login/utils/request.js b/src/themes/react/static/login/utils/request.js new file mode 100644 index 000000000..95c6cf564 --- /dev/null +++ b/src/themes/react/static/login/utils/request.js @@ -0,0 +1,48 @@ +/* Copyright IBM Corp, All Rights Reserved. + + SPDX-License-Identifier: Apache-2.0 + */ + +import axios from 'axios' +import qs from 'qs' +import config from './config' + +const fetch = (options) => { + const { + method = 'get', + data, + url, + } = options + switch (method.toLowerCase()) { + case 'get': + return axios.get(`${url}${options.data ? `?${qs.stringify(options.data)}` : ''}`) + case 'delete': + return axios.delete(url, { data }) + case 'head': + return axios.head(url, data) + case 'post': + return axios.post(url, qs.stringify(data)) + case 'put': + return axios.put(url, qs.stringify(data)) + case 'patch': + return axios.patch(url, data) + default: + return axios(options) + } +} + +export default function request (options) { + return fetch(options).then((response) => { + const { statusText, status } = response + let data = options.isCross ? response.data.query.results.json : response.data + return { + code: 0, + status, + message: statusText, + ...data, + } + }).catch((error) => { + const { response = { statusText: 'Network Error' } } = error + return { code: 1, message: response.statusText } + }) +} diff --git a/src/themes/react/static/login/utils/theme.js b/src/themes/react/static/login/utils/theme.js new file mode 100644 index 000000000..c25585c4d --- /dev/null +++ b/src/themes/react/static/login/utils/theme.js @@ -0,0 +1,14 @@ +module.exports = { + color: { + green: '#64ea91', + blue: '#8fc9fb', + purple: '#d897eb', + red: '#f69899', + yellow: '#f8c82e', + peach: '#f797d6', + borderBase: '#e5e5e5', + borderSplit: '#f4f4f4', + grass: '#d6fbb5', + sky: '#c1e0fc', + }, +} diff --git a/src/themes/react/static/package.json b/src/themes/react/static/package.json index 002a3c008..001a25d56 100644 --- a/src/themes/react/static/package.json +++ b/src/themes/react/static/package.json @@ -1,7 +1,8 @@ { "private": true, "entry": { - "index": "./js/index.js" + "index": "./js/index.js", + "login": "./login/index.js" }, "dependencies": { "antd": "^2.6.0", diff --git a/src/themes/react/templates/index.html b/src/themes/react/templates/index.html index 5058d534f..26f0ff991 100644 --- a/src/themes/react/templates/index.html +++ b/src/themes/react/templates/index.html @@ -31,6 +31,10 @@ + diff --git a/src/themes/react/templates/login.html b/src/themes/react/templates/login.html new file mode 100644 index 000000000..81faabcc8 --- /dev/null +++ b/src/themes/react/templates/login.html @@ -0,0 +1,230 @@ + + + + + + + + + + + + + + + + + {% block head %} + Cello Dashboard - Login + {% endblock %} + + + + + + +
+ + + + + + + + + +