diff --git a/documents/Principle_zh-CN.md b/documents/Principle_zh-CN.md index 451e0b1..d993b3e 100644 --- a/documents/Principle_zh-CN.md +++ b/documents/Principle_zh-CN.md @@ -153,33 +153,6 @@ Common类下的为通用请求接口,一般与用户状态等相关 返回参数:一个bool,表示是否有效 -#### isAdmin - -请求地址:`/api/common/isAdmin` - -请求参数: - -|参数名|参数类型|参数解释| -|:---|:---|:---| -|token|string|用于管理员鉴别的token| - -返回参数:一个bool,表示是否是管理员 - -#### changePassword - -请求地址:`/api/common/changePassword` - -请求参数: - -|参数名|参数类型|参数解释| -|:---|:---|:---| -|token|string|用于用户鉴权的字符串| -|password|string|新的明文密码| - -返回参数:一个bool,表示是否修改成功 - -此请求的安全性由HTTPS保证。 - ### Calendar类 Calendar类下的为日历请求接口 @@ -568,6 +541,63 @@ Admin类的操作不涉及任何客户端存储,因此不需要lastChange来 返回参数:一个bool表示是否删除成功 +### Profile类 + +Admin类下的为当前用户一些个人属性的请求接口 +Profile类的操作不涉及任何客户端存储,因此不需要lastChange来保护。 + +#### isAdmin + +请求地址:`/api/profile/isAdmin` + +请求参数: + +|参数名|参数类型|参数解释| +|:---|:---|:---| +|token|string|用于管理员鉴别的token| + +返回参数:一个bool,表示是否是管理员 + +#### changePassword + +请求地址:`/api/profile/changePassword` + +请求参数: + +|参数名|参数类型|参数解释| +|:---|:---|:---| +|token|string|用于用户鉴权的字符串| +|password|string|新的明文密码| + +返回参数:一个bool,表示是否修改成功 + +此请求的安全性由HTTPS保证。 + +#### getToken + +请求地址:`/api/profile/getToken` + +请求参数: + +|参数名|参数类型|参数解释| +|:---|:---|:---| +|token|string|用于用户鉴权的字符串| + +返回参数:一个JSON列表,为token表中符合当前提起请求的用户的所有token的条目 + +#### deleteToken + +请求地址:`/api/profile/deleteToken` + +请求参数: + +|参数名|参数类型|参数解释| +|:---|:---|:---| +|token|string|用于用户鉴权的字符串| +|deleteToken|string|需要被强制下线的token| + +返回参数:一个bool,表示是否下线成功 + ## 事件循环规则字符串 事件循环规则字符串 是一串用于描述当前事件循环规则的字符串,通过解析字符串可以计算出整个时间序列。本字符串借鉴了ics设计,但与ics设计毫无相似之处。 diff --git a/src/database.py b/src/database.py index 47f7567..d8ed454 100644 --- a/src/database.py +++ b/src/database.py @@ -136,7 +136,7 @@ class CalendarDatabase(object): return salt @SafeDatabaseOperation - def common_login(self, username, password): + def common_login(self, username, password, clientUa, clientIp): self.cursor.execute('SELECT [ccn_password], [ccn_salt] FROM user WHERE [ccn_name] = ?;', (username, )) (gotten_salt, gotten_password) = self.cursor.fetchone() @@ -146,10 +146,12 @@ class CalendarDatabase(object): utils.GenerateSalt(), # regenerate a new slat to prevent re-login try username )) - self.cursor.execute('INSERT INTO token VALUES (?, ?, ?);', ( + self.cursor.execute('INSERT INTO token VALUES (?, ?, ?, ?, ?);', ( username, token, utils.GetTokenExpireOn(), # add 2 day from now + clientUa, + clientIp, )) return token else: @@ -157,15 +159,17 @@ class CalendarDatabase(object): raise Exception('Login authentication failed') @SafeDatabaseOperation - def common_webLogin(self, username, password): + def common_webLogin(self, username, password, clientUa, clientIp): self.cursor.execute('SELECT [ccn_name] FROM user WHERE [ccn_name] = ? AND [ccn_password] = ?;', (username, utils.ComputePasswordHash(password))) if len(self.cursor.fetchall()) != 0: token = utils.GenerateToken(username) - self.cursor.execute('INSERT INTO token VALUES (?, ?, ?);', ( + self.cursor.execute('INSERT INTO token VALUES (?, ?, ?, ?, ?);', ( username, token, utils.GetTokenExpireOn(), # add 2 day from now + clientUa, + clientIp, )) return token else: @@ -183,20 +187,6 @@ class CalendarDatabase(object): self.tokenOper_check_valid(token) return True - @SafeDatabaseOperation - def common_isAdmin(self, token): - username = self.tokenOper_get_username(token) - return self.tokenOper_is_admin(username) - - @SafeDatabaseOperation - def common_changePassword(self, token, newpassword): - username = self.tokenOper_get_username(token) - self.cursor.execute('UPDATE user SET [ccn_password] = ? WHERE [ccn_name] = ?;', ( - utils.ComputePasswordHash(newpassword), - username - )) - return True - # =============================== calendar @SafeDatabaseOperation def calendar_getFull(self, token, startDateTime, endDateTime): @@ -559,3 +549,40 @@ class CalendarDatabase(object): raise Exception('Fail to delete due to no matched rows or too much rows.') return True + # =============================== profile + @SafeDatabaseOperation + def profile_isAdmin(self, token): + username = self.tokenOper_get_username(token) + return self.tokenOper_is_admin(username) + + @SafeDatabaseOperation + def profile_changePassword(self, token, newpassword): + username = self.tokenOper_get_username(token) + self.cursor.execute('UPDATE user SET [ccn_password] = ? WHERE [ccn_name] = ?;', ( + utils.ComputePasswordHash(newpassword), + username + )) + return True + + @SafeDatabaseOperation + def profile_getToken(self, token): + username = self.tokenOper_get_username(token) + + self.cursor.execute('SELECT * FROM token WHERE [ccn_user] = ?;', ( + username, + )) + return self.cursor.fetchall() + + @SafeDatabaseOperation + def profile_deleteToken(self, token, deleteToken): + _username = self.tokenOper_get_username(token) + + # delete + self.cursor.execute('DELETE FROM token WHERE [ccn_user] = ? AND [ccn_token] = ?;', ( + _username, + deleteToken + )) + if self.cursor.rowcount != 1: + raise Exception('Fail to delete due to no matched rows or too much rows.') + return True + diff --git a/src/server.py b/src/server.py index b7ee68c..951ca39 100644 --- a/src/server.py +++ b/src/server.py @@ -92,40 +92,58 @@ def web_eventUpdateHandle(uuidPath): @app.route('/api/common/salt', methods=['POST']) def api_common_saltHandle(): return SmartDbCaller(calendar_db.common_salt, - (('username', str, False), )) + (('username', str, False), ), + None) @app.route('/api/common/login', methods=['POST']) def api_common_loginHandle(): + # construct client data first + clientUa = request.user_agent.string + if request.headers.getlist("X-Forwarded-For"): + clientIp = request.headers.getlist("X-Forwarded-For")[0] + else: + clientIp = request.remote_addr + return SmartDbCaller(calendar_db.common_login, (('username', str, False), - ('password', str, False))) + ('password', str, False), + ('clientUa', str, False), + ('clientIp', str, False)), + { + 'clientUa': clientUa, + 'clientIp': clientIp + }) @app.route('/api/common/webLogin', methods=['POST']) def api_common_webLoginHandle(): + # construct client data first + clientUa = request.user_agent.string + if request.headers.getlist("X-Forwarded-For"): + clientIp = request.headers.getlist("X-Forwarded-For")[0] + else: + clientIp = request.remote_addr + return SmartDbCaller(calendar_db.common_webLogin, (('username', str, False), - ('password', str, False))) + ('password', str, False), + ('clientUa', str, False), + ('clientIp', str, False)), + { + 'clientUa': clientUa, + 'clientIp': clientIp + }) @app.route('/api/common/logout', methods=['POST']) def api_common_logoutHandle(): return SmartDbCaller(calendar_db.common_logout, - (('token', str, False), )) + (('token', str, False), ), + None) @app.route('/api/common/tokenValid', methods=['POST']) def api_common_tokenValidHandle(): return SmartDbCaller(calendar_db.common_tokenValid, - (('token', str, False), )) - -@app.route('/api/common/isAdmin', methods=['POST']) -def api_common_isAdminHandle(): - return SmartDbCaller(calendar_db.common_isAdmin, - (('token', str, False), )) - -@app.route('/api/common/changePassword', methods=['POST']) -def api_common_changePasswordHandle(): - return SmartDbCaller(calendar_db.common_changePassword, - (('token', str, False), - ('password', str, False))) + (('token', str, False), ), + None) # ================================ calendar @@ -134,20 +152,23 @@ def api_calendar_getFullHandle(): return SmartDbCaller(calendar_db.calendar_getFull, (('token', str, False), ('startDateTime', int, False), - ('endDateTime', int, False))) + ('endDateTime', int, False)), + None) @app.route('/api/calendar/getList', methods=['POST']) def api_calendar_getListHandle(): return SmartDbCaller(calendar_db.calendar_getList, (('token', str, False), ('startDateTime', int, False), - ('endDateTime', int, False))) + ('endDateTime', int, False)), + None) @app.route('/api/calendar/getDetail', methods=['POST']) def api_calendar_getDetailHandle(): return SmartDbCaller(calendar_db.calendar_getDetail, (('token', str, False), - ('uuid', str, False))) + ('uuid', str, False)), + None) @app.route('/api/calendar/update', methods=['POST']) def api_calendar_updateHandle(): @@ -161,7 +182,8 @@ def api_calendar_updateHandle(): ('eventDateTimeEnd', int, True), ('loopRules', str, True), ('timezoneOffset', int, True), - ('lastChange', str, False))) + ('lastChange', str, False)), + None) @app.route('/api/calendar/add', methods=['POST']) def api_calendar_addHandle(): @@ -173,38 +195,44 @@ def api_calendar_addHandle(): ('eventDateTimeStart', int, False), ('eventDateTimeEnd', int, False), ('loopRules', str, False), - ('timezoneOffset', int, False))) + ('timezoneOffset', int, False)), + None) @app.route('/api/calendar/delete', methods=['POST']) def api_calendar_deleteHandle(): return SmartDbCaller(calendar_db.calendar_delete, (('token', str, False), ('uuid', str, False), - ('lastChange', str, False))) + ('lastChange', str, False)), + None) # ================================ collection @app.route('/api/collection/getFullOwn', methods=['POST']) def api_collection_getFullOwnHandle(): return SmartDbCaller(calendar_db.collection_getFullOwn, - (('token', str, False), )) + (('token', str, False), ), + None) @app.route('/api/collection/getListOwn', methods=['POST']) def api_collection_getListOwnHandle(): return SmartDbCaller(calendar_db.collection_getListlOwn, - (('token', str, False), )) + (('token', str, False), ), + None) @app.route('/api/collection/getDetailOwn', methods=['POST']) def api_collection_getDetailOwnHandle(): return SmartDbCaller(calendar_db.collection_getDetailOwn, (('token', str, False), - ('uuid', str, False))) + ('uuid', str, False)), + None) @app.route('/api/collection/addOwn', methods=['POST']) def api_collection_addOwnHandle(): return SmartDbCaller(calendar_db.collection_addOwn, (('token', str, False), - ('name', str, False))) + ('name', str, False)), + None) @app.route('/api/collection/updateOwn', methods=['POST']) def api_collection_updateOwnHandle(): @@ -212,21 +240,24 @@ def api_collection_updateOwnHandle(): (('token', str, False), ('uuid', str, False), ('name', str, False), - ('lastChange', str, False))) + ('lastChange', str, False)), + None) @app.route('/api/collection/deleteOwn', methods=['POST']) def api_collection_deleteOwnHandle(): return SmartDbCaller(calendar_db.collection_deleteOwn, (('token', str, False), ('uuid', str, False), - ('lastChange', str, False))) + ('lastChange', str, False)), + None) @app.route('/api/collection/getSharing', methods=['POST']) def api_collection_getSharingHandle(): return SmartDbCaller(calendar_db.collection_getSharing, (('token', str, False), - ('uuid', str, False))) + ('uuid', str, False)), + None) @app.route('/api/collection/deleteSharing', methods=['POST']) def api_collection_deleteSharingHandle(): @@ -234,7 +265,8 @@ def api_collection_deleteSharingHandle(): (('token', str, False), ('uuid', str, False), ('target', str, False), - ('lastChange', str, False))) + ('lastChange', str, False)), + None) @app.route('/api/collection/addSharing', methods=['POST']) def api_collection_addSharingHandle(): @@ -242,37 +274,42 @@ def api_collection_addSharingHandle(): (('token', str, False), ('uuid', str, False), ('target', str, False), - ('lastChange', str, False))) + ('lastChange', str, False)), + None) @app.route('/api/collection/getShared', methods=['POST']) def api_collection_getSharedHandle(): return SmartDbCaller(calendar_db.collection_getShared, - (('token', str, False), )) - + (('token', str, False), ), + None) # ================================ todo @app.route('/api/todo/getFull', methods=['POST']) def api_todo_getFullHandle(): return SmartDbCaller(calendar_db.todo_getFull, - (('token', str, False), )) + (('token', str, False), ), + None) @app.route('/api/todo/getList', methods=['POST']) def api_todo_getListHandle(): return SmartDbCaller(calendar_db.todo_getList, - (('token', str, False), )) + (('token', str, False), ), + None) @app.route('/api/todo/getDetail', methods=['POST']) def api_todo_getDetailHandle(): return SmartDbCaller(calendar_db.todo_getDetail, (('token', str, False), - ('uuid', str, False))) + ('uuid', str, False)), + None) @app.route('/api/todo/add', methods=['POST']) def api_todo_addHandle(): return SmartDbCaller(calendar_db.todo_add, - (('token', str, False), )) + (('token', str, False), ), + None) @app.route('/api/todo/update', methods=['POST']) def api_todo_updateHandle(): @@ -280,27 +317,31 @@ def api_todo_updateHandle(): (('token', str, False), ('uuid', str, False), ('data', str, False), - ('lastChange', str, False))) + ('lastChange', str, False)), + None) @app.route('/api/todo/delete', methods=['POST']) def api_todo_deleteHandle(): return SmartDbCaller(calendar_db.todo_delete, (('token', str, False), ('uuid', str, False), - ('lastChange', str, False))) + ('lastChange', str, False)), + None) # ================================ admin @app.route('/api/admin/get', methods=['POST']) def api_admin_getHandle(): return SmartDbCaller(calendar_db.admin_get, - (('token', str, False), )) + (('token', str, False), ), + None) @app.route('/api/admin/add', methods=['POST']) def api_admin_addHandle(): return SmartDbCaller(calendar_db.admin_add, (('token', str, False), - ('username', str, False))) + ('username', str, False)), + None) @app.route('/api/admin/update', methods=['POST']) def api_admin_updateHandle(): @@ -308,13 +349,43 @@ def api_admin_updateHandle(): (('token', str, False), ('username', str, False), ('password', str, True), - ('isAdmin', utils.Str2Bool, True))) + ('isAdmin', utils.Str2Bool, True)), + None) @app.route('/api/admin/delete', methods=['POST']) def api_admin_deleteHandle(): return SmartDbCaller(calendar_db.admin_delete, (('token', str, False), - ('username', str, False))) + ('username', str, False)), + None) + +# ================================ profile + +@app.route('/api/profile/isAdmin', methods=['POST']) +def api_profile_isAdminHandle(): + return SmartDbCaller(calendar_db.profile_isAdmin, + (('token', str, False), ), + None) + +@app.route('/api/profile/changePassword', methods=['POST']) +def api_profile_changePasswordHandle(): + return SmartDbCaller(calendar_db.profile_changePassword, + (('token', str, False), + ('password', str, False)), + None) + +@app.route('/api/profile/getToken', methods=['POST']) +def api_profile_getTokenHandle(): + return SmartDbCaller(calendar_db.profile_getToken, + (('token', str, False), ), + None) + +@app.route('/api/profile/deleteToken', methods=['POST']) +def api_profile_deleteTokenHandle(): + return SmartDbCaller(calendar_db.profile_deleteToken, + (('token', str, False), + ('deleteToken', str, False)), + None) # =============================================main run @@ -336,14 +407,20 @@ def UpdateStaticResources(): } ''' -def SmartDbCaller(dbMethod, paramTuple): +def SmartDbCaller(dbMethod, paramTuple, extraDict): result = (False, 'Invalid parameter', None) optCount = 0 paramList = [] optParamDict = {} - # for each item, item[0] is field name. item[1] is type. item[2] is whether it is optional field + # for each item, + # item[0] is field name. + # item[1] is type. + # item[2] is whether it is optional field + realForm = request.form.to_dict() + if extraDict is not None: + realForm.update(extraDict) for item in paramTuple: - cache = request.form.get(item[0], default=None, type=item[1]) + cache = item[1](realForm.get(item[0], None)) if item[2]: # optional param if cache is not None: diff --git a/src/sql/sqlite.sql b/src/sql/sqlite.sql index e8aab63..ffcd44a 100644 --- a/src/sql/sqlite.sql +++ b/src/sql/sqlite.sql @@ -11,6 +11,8 @@ CREATE TABLE token( [ccn_user] TEXT NOT NULL, [ccn_token] TEXT UNIQUE NOT NULL, [ccn_tokenExpireOn] BIGINT NOT NULL, + [ccn_ua] TEXT NOT NULL, + [ccn_ip] TEXT NOT NULL, FOREIGN KEY (ccn_user) REFERENCES user(ccn_name) ON DELETE CASCADE ); diff --git a/src/static/css/admin.css b/src/static/css/admin.css index de24313..b507dd6 100644 --- a/src/static/css/admin.css +++ b/src/static/css/admin.css @@ -24,6 +24,31 @@ div.user-item-icon { +div.token-item { + display: flex; + flex-flow: row; + align-items: flex-start; + + padding: 1.25rem; + margin-bottom: 1.25rem; +} + +div.token-item-words { + display: flex; + flex-flow: column; + align-items: flex-start; + flex-grow: 1; + flex-basis: 0; + + word-break: break-all; +} + +div.token-item-icon { + margin-left: 0.75rem; +} + + + div.control-list { display: flex; diff --git a/src/static/i18n/strings_en-US.properties b/src/static/i18n/strings_en-US.properties index 1598dda..6d97250 100644 --- a/src/static/i18n/strings_en-US.properties +++ b/src/static/i18n/strings_en-US.properties @@ -134,4 +134,5 @@ ccn-i18n-userItem-isAdmin=Is Admin ccn-i18n-tokenItem-ua=User Agent: ccn-i18n-tokenItem-ip=IP: +ccn-i18n-tokenItem-expireOn=Expire On: ccn-i18n-tokenItem-isMe=This is the login credentials you are currently using. diff --git a/src/static/i18n/strings_zh-CN.properties b/src/static/i18n/strings_zh-CN.properties index 3cff856..1dda84a 100644 --- a/src/static/i18n/strings_zh-CN.properties +++ b/src/static/i18n/strings_zh-CN.properties @@ -134,4 +134,5 @@ ccn-i18n-userItem-isAdmin=是管理员 ccn-i18n-tokenItem-ua=UA: ccn-i18n-tokenItem-ip=IP: +ccn-i18n-tokenItem-expireOn=过期时间: ccn-i18n-tokenItem-isMe=这是你当前使用的登录凭据 diff --git a/src/static/js/api.js b/src/static/js/api.js index 18a5ee3..85ac94d 100644 --- a/src/static/js/api.js +++ b/src/static/js/api.js @@ -179,25 +179,6 @@ function ccn_api_common_tokenValid() { } } -function ccn_api_common_isAdmin() { - return ccn_api_boolTemplate( - '/api/common/isAdmin', - { - token: ccn_localstorageAssist_GetApiToken() - } - ); -} - -function ccn_api_common_changePassword(_password) { - return ccn_api_boolTemplate( - '/api/common/changePassword', - { - token: ccn_localstorageAssist_GetApiToken(), - password: _password - } - ); -} - // ====================================================== calendar function ccn_api_calendar_getFull(_startDateTime, _endDateTime) { @@ -461,3 +442,42 @@ function ccn_api_admin_delete(_username) { ); } +// ====================================================== profile + +function ccn_api_profile_isAdmin() { + return ccn_api_boolTemplate( + '/api/profile/isAdmin', + { + token: ccn_localstorageAssist_GetApiToken() + } + ); +} + +function ccn_api_profile_changePassword(_password) { + return ccn_api_boolTemplate( + '/api/profile/changePassword', + { + token: ccn_localstorageAssist_GetApiToken(), + password: _password + } + ); +} + +function ccn_api_profile_getToken() { + return ccn_api_boolTemplate( + '/api/profile/getToken', + { + token: ccn_localstorageAssist_GetApiToken() + } + ); +} + +function ccn_api_profile_deleteToken(_deleteToken) { + return ccn_api_boolTemplate( + '/api/profile/deleteToken', + { + token: ccn_localstorageAssist_GetApiToken(), + deleteToken: _deleteToken + } + ); +} diff --git a/src/static/js/page/admin.js b/src/static/js/page/admin.js index 6acddaa..0cd04eb 100644 --- a/src/static/js/page/admin.js +++ b/src/static/js/page/admin.js @@ -1,4 +1,5 @@ var ccn_admin_userListCache = []; +var ccn_admin_tokenListCache = []; $(document).ready(function() { ccn_pages_currentPage = ccn_pages_enumPages.admin; @@ -28,7 +29,7 @@ $(document).ready(function() { ccn_tabcontrol_SwitchTab(1, 1); // load user tab according to admin status - if(!ccn_api_common_isAdmin()) + if(!ccn_api_profile_isAdmin()) $('#tabcontrol-tab-1-3').hide(); // apply i18n @@ -37,6 +38,7 @@ $(document).ready(function() { // bind event $('#ccn-admin-profile-btnChangePassword').click(ccn_admin_profile_ChangePassword); + $('#ccn-admin-tokenList-btnRefresh').click(ccn_admin_tokenList_Refresh); $('#ccn-admin-userList-btnAdd').click(ccn_admin_userList_Add); $('#ccn-admin-userList-btnRefresh').click(ccn_admin_userList_Refresh); }); @@ -47,7 +49,7 @@ function ccn_admin_profile_ChangePassword() { var newpassword = $('#ccn-admin-profile-inputPassword').val(); if (newpassword == "") return; - var result = ccn_api_common_changePassword(newpassword); + var result = ccn_api_profile_changePassword(newpassword); if(result) { ccn_messagebox_Show($.i18n.prop("ccn-i18n-js-success")); $('#ccn-admin-profile-inputPassword').val(''); @@ -56,6 +58,61 @@ function ccn_admin_profile_ChangePassword() { } +// ================== token + +function ccn_admin_tokenList_Refresh() { + ccn_admin_tokenListCache = new Array(); + var listDOM = $('#ccn-admin-tokenList'); + listDOM.empty(); + + var renderdata = { + uuid: undefined, + isMe: undefined, + ua: undefined, + ip: undefined, + expireOn: undefined + } + var gottenDateTime = new Date(); + + var result = ccn_api_profile_getToken(); + if(typeof(result) != 'undefined') { + for(var index in result) { + var item = result[index]; + renderdata.uuid = item[1]; + renderdata.isMe = ccn_localstorageAssist_GetApiToken() == item[1]; + renderdata.ua = item[3]; + renderdata.ip = item[4]; + gottenDateTime.setTime(item[2] * 1000); + renderdata.expireOn = gottenDateTime.toLocaleString(); + + listDOM.append(ccn_template_tokenItem.render(renderdata)); + + // bind event + var uuid = renderdata.uuid; + $("#ccn-tokenItem-btnLogout-" + uuid).click(ccn_admin_tokenList_ItemDelete); + + // add into cache + ccn_admin_tokenListCache[uuid] = item; + } + + ccn_i18n_ApplyLanguage2Content(listDOM); + } +} + +function ccn_admin_tokenList_ItemDelete() { + var uuid = $(this).attr("uuid"); + + var result = ccn_api_profile_deleteToken(uuid); + + if(!result) { + // fail + ccn_messagebox_Show($.i18n.prop("ccn-i18n-js-fail-delete")); + } else { + // remove body + $("#ccn-tokenItem-" + uuid).remove(); + } +} + // ================== user list function ccn_admin_userList_RefreshCacheList() { diff --git a/src/static/tmpl/tokenItem.tmpl b/src/static/tmpl/tokenItem.tmpl index 6f109fc..3af5149 100644 --- a/src/static/tmpl/tokenItem.tmpl +++ b/src/static/tmpl/tokenItem.tmpl @@ -1,9 +1,6 @@
{{>ua}} @@ -12,6 +9,16 @@ {{>ip}}
++ + {{>expireOn}} +
+ {{if isMe}} ++ + +
+ {{/if}}