diff --git a/.gitignore b/.gitignore index 4fb5edc..d59593c 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,7 @@ src/__pycache__ # ignore any image first *.png *.jpg -*.gif \ No newline at end of file +*.gif + +# elimate vscode +.vscode \ No newline at end of file diff --git a/src/database.py b/src/database.py index 2459469..2391ab0 100644 --- a/src/database.py +++ b/src/database.py @@ -105,6 +105,23 @@ class CalendarDatabase(object): (gotten_salt, gotten_password) = self.cursor.fetchone() if password == utils.ComputePasswordHashWithSalt(gotten_password, gotten_salt): + token = utils.GenerateToken(username) + self.cursor.execute('UPDATE user SET [ccn_token] = ?, [ccn_tokenExpireOn] = ?, [ccn_salt] = ? WHERE [ccn_name] = ?;', ( + token, + utils.GetCurrentTimestamp() + 60 * 60 * 24 * 2, # add 2 day from now + utils.GenerateSalt(), # regenerate a new slat to prevent re-login try + username + )) + return token + else: + # return empty string to indicate fail to login + return '' + + @SafeDatabaseOperation + def common_webLogin(self, username, password): + 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('UPDATE user SET [ccn_token] = ?, [ccn_tokenExpireOn] = ? WHERE [ccn_name] = ?;', ( token, @@ -118,15 +135,18 @@ class CalendarDatabase(object): @SafeDatabaseOperation def common_logout(self, token): - username = self.get_username_from_token(cur, token) + username = self.get_username_from_token(token) self.cursor.execute('UPDATE user SET [ccn_tokenExpireOn] = 0 WHERE [ccn_name] = ?;', (username, )) - return None + return True @SafeDatabaseOperation def common_tokenValid(self, token): # get user name have check the validation, don't do anything more. - self.get_username_from_token(token) - return result + try: + self.get_username_from_token(token) + return True + except: + return False @SafeDatabaseOperation def common_isAdmin(self, token): @@ -142,7 +162,7 @@ class CalendarDatabase(object): newpassword, username )) - return None + return True # =============================== calendar @@ -151,8 +171,71 @@ class CalendarDatabase(object): # =============================== todo + @SafeDatabaseOperation + def todo_getFull(self, token): + username = self.get_username_from_token(token) + self.cursor.execute('SELECT * FROM todo WHERE [ccn_belongTo] = ?;', (username, )) + return self.cursor.fetchall() + @SafeDatabaseOperation + def todo_getList(self, token): + username = self.get_username_from_token(token) + self.cursor.execute('SELECT [ccn_uuid] FROM todo WHERE [ccn_belongTo] = ?;', (username, )) + return tuple(map(lambda x: x[0], self.cursor.fetchall())) + @SafeDatabaseOperation + def todo_getDetail(self, token, uuid): + username = self.get_username_from_token(token) + self.cursor.execute('SELECT * FROM todo WHERE [ccn_belongTo] = ? AND [ccn_uuid] = ?;', (username, uuid)) + return self.cursor.fetchone() + + @SafeDatabaseOperation + def todo_add(self, token): + username = self.get_username_from_token(token) + newuuid = utils.GenerateUUID() + lastupdate = utils.GenerateUUID() + self.cursor.execute('INSERT INTO todo VALUES (?, ?, ?, ?);', ( + newuuid, + username, + '', + lastupdate, + )) + return newuuid + + @SafeDatabaseOperation + def todo_update(self, token, uuid, data, lastChange): + # check valid token + self.get_username_from_token(token) + # check sync conflict + self.cursor.execute('SELECT [ccn_uuid] FROM todo WHERE [ccn_uuid] = ? AND [ccn_lastChange] = ?;', ( + uuid, + lastChange + )) + if len(self.cursor.fetchall()) == 0: + return False + + # update + self.cursor.execute('UPDATE todo SET [ccn_data] = ? WHERE [ccn_uuid] = ?;', ( + data, + uuid + )) + return True + + @SafeDatabaseOperation + def todo_delete(self, token, uuid, lastChange): + # check valid token + self.get_username_from_token(token) + # check sync conflict + self.cursor.execute('SELECT [ccn_uuid] FROM todo WHERE [ccn_uuid] = ? AND [ccn_lastChange] = ?;', ( + uuid, + lastChange + )) + if len(self.cursor.fetchall()) == 0: + return False + + # delete + self.cursor.execute('DELETE FROM todo WHERE [ccn_uuid] = ?;', (uuid, )) + return True # =============================== admin diff --git a/src/server.py b/src/server.py index a801a28..12a2b9a 100644 --- a/src/server.py +++ b/src/server.py @@ -68,19 +68,54 @@ def web_loginHandle(): @app.route('/api/common/salt', methods=['POST']) def api_common_saltHandle(): - pass + result = (False, None) + if (CheckParameter(('username', ))): + db = get_database() + result = db.common_salt(request.form['username']) + + return ConstructResponseBody(result) @app.route('/api/common/login', methods=['POST']) def api_common_loginHandle(): - pass + result = (False, None) + if (CheckParameter(('username', 'password'))): + db = get_database() + result = db.common_login( + request.form['username'], + request.form['password'] + ) + + return ConstructResponseBody(result) + +@app.route('/api/common/webLogin', methods=['POST']) +def api_common_webLoginHandle(): + result = (False, None) + if (CheckParameter(('username', 'password'))): + db = get_database() + result = db.common_webLogin( + request.form['username'], + request.form['password'] + ) + + return ConstructResponseBody(result) @app.route('/api/common/logout', methods=['POST']) def api_common_logoutHandle(): - pass + result = (False, None) + if (CheckParameter(('token', ))): + db = get_database() + result = db.common_logout(request.form['token']) + + return ConstructResponseBody(result) @app.route('/api/common/tokenValid', methods=['POST']) def api_common_tokenValidHandle(): - pass + result = (False, None) + if (CheckParameter(('token', ))): + db = get_database() + result = db.common_tokenValid(request.form['token']) + + return ConstructResponseBody(result) @app.route('/api/common/isAdmin', methods=['POST']) def api_common_isAdminHandle(): @@ -217,6 +252,18 @@ def UpdateStaticResources(): } ''' +def CheckParameter(paramList): + gotten = set(request.form.keys()) + paramSet = set(paramList) + return gotten.issubset(paramSet) and paramSet.issubset(gotten) + +def ConstructResponseBody(returnedTuple): + return { + 'success': returnedTuple[0], + 'error': '', + 'data': returnedTuple[1] + } + def run(): app.run(port=config.CustomConfig['web']['port']) \ No newline at end of file diff --git a/src/static/i18n/strings_en-US.properties b/src/static/i18n/strings_en-US.properties index 67a16cd..096e09a 100644 --- a/src/static/i18n/strings_en-US.properties +++ b/src/static/i18n/strings_en-US.properties @@ -12,6 +12,9 @@ ccn-header-user-login=Login ccn-header-user-logout=Logout ccn-header-language=Language +ccn-js-failToLogin=Fail to login. Please check your username or password. +ccn-js-failToLogout=Fail to logout due to unknow reason. Consider refreshing page to solve problem. + ccn-home-desc=
A light, self-host calendar system.
Originally, this app is served for yyc12345 personal use.
Pull request / issue / translation are welcomed.
Submit them in our GitHub project.
This project source code is licensed AGPL v3.
ccn-login-form-username=Username diff --git a/src/static/i18n/strings_zh-CN.properties b/src/static/i18n/strings_zh-CN.properties index 46fe982..fd3a6ee 100644 --- a/src/static/i18n/strings_zh-CN.properties +++ b/src/static/i18n/strings_zh-CN.properties @@ -12,6 +12,9 @@ ccn-header-user-login=登录 ccn-header-user-logout=登出 ccn-header-language=语言 +ccn-js-failToLogin=登陆失败,请检查您的用户名和密码。 +ccn-js-failToLogout=由于未知原因,登出失败,请考虑刷新页面解决问题。 + ccn-home-desc=一个轻量的自建日历系统
原本是出于yyc12345的个人使用而制作的。
欢迎提出Pull request / issue / 翻译
将他们提交到我们的GitHub项目.
本工程代码使用AGPL v3授权。
ccn-login-form-username=用户名 diff --git a/src/static/js/api.js b/src/static/js/api.js index 131a11f..1b73236 100644 --- a/src/static/js/api.js +++ b/src/static/js/api.js @@ -1,3 +1,117 @@ -function cnn_api_tokenValid() { - return true; +// var cached_salt = undefined + +/* +function cnn_api_common_salt(_username) { + // true or false + // gotten salt store in cached_salt. + var gotten_data = undefined; + $.ajax({ + url: '/api/common/salt', + type: "POST", + async: false, + data: { + username: _username + }, + success: function (data) { + gotten_data = data; + } + }); + + if (IsResponseOK(gotten_data)) { + cached_salt = gotten_data['data']; + return true; + } else return false; +} + +function cnn_api_common_login(_username, password) { + // return true or false, token is managed by this js file self. + // if cached_salt is undefined, return false directly + if (typeof(cached_salt) == undefined) return false; + + var gotten_data = undefined; + $.ajax({ + url: '/api/common/login', + type: "POST", + async: false, + data: { + username: _username, + password: ComputPasswordWithSalt(password, cached_salt) + }, + success: function (data) { + gotten_data = data; + } + }); + if (IsResponseOK(gotten_data) && gotten_data['data'] != '') { + SetApiToken(gotten_data['data']); + cached_salt = undefined; + return true; + } else return false; +} +*/ + +function cnn_api_common_webLogin(_username, password) { + var gotten_data = undefined; + $.ajax({ + url: '/api/common/webLogin', + type: "POST", + async: false, + data: { + username: _username, + password: password + }, + success: function (data) { + gotten_data = data; + } + }); + if (IsResponseOK(gotten_data) && gotten_data['data'] != '') { + SetApiToken(gotten_data['data']); + return true; + } else return false; +} + +function cnn_api_common_logout() { + // return true or false + var gotten_data = undefined; + $.ajax({ + url: '/api/common/logout', + type: "POST", + async: false, + data: { + token: GetApiToken() + }, + success: function (data) { + gotten_data = data; + } + }); + + if (IsResponseOK(gotten_data) && gotten_data['data']) { + SetApiToken(''); + return true; + } return false; +} + +function cnn_api_common_tokenValid() { + // get from local database first, then judge it via post + // return true or false + var gotten_token = GetApiToken(); + if (gotten_token == '') return false; + + var gotten_data = undefined; + $.ajax({ + url: '/api/common/tokenValid', + type: "POST", + async: false, + data: { + token: GetApiToken() + }, + success: function (data) { + gotten_data = data; + } + }); + + if (IsResponseOK(gotten_data) && gotten_data['data']) return true; + else { + SetApiToken(''); + return false; + } } \ No newline at end of file diff --git a/src/static/js/headerNav.js b/src/static/js/headerNav.js index 3b2e9ed..d71d576 100644 --- a/src/static/js/headerNav.js +++ b/src/static/js/headerNav.js @@ -11,7 +11,7 @@ function cnn_headerNav_Insert() { } function cnn_headerNav_LoggedRefresh() { - if (cnn_api_tokenValid()) { + if (cnn_api_common_tokenValid()) { // logged, show all nav button and logout button $("#cnn-header-nav-home").show(); $("#cnn-header-nav-calendar").show(); @@ -41,8 +41,16 @@ function cnn_headerNav_BindEvents() { }); }); - // todo: bind logout - + // bind logout + $("#cnn-header-user-logout").click(function() { + if (cnn_api_common_logout()) { + // ok, logout + // jump into home page again + window.location.href = '/web/home'; + return; + + } else alert($.i18n.prop("ccn-js-failToLogout")); + }); // bind burger menu // copy from bulma website diff --git a/src/static/js/page/login.js b/src/static/js/page/login.js index 0685097..7289ef8 100644 --- a/src/static/js/page/login.js +++ b/src/static/js/page/login.js @@ -5,6 +5,45 @@ $(document).ready(function() { cnn_headerNav_BindEvents(); cnn_headerNav_LoggedRefresh(); + // bind login event + $("#ccn-login-form-login").click(StartLogin); + // apply i18n ccn_i18n_ApplyLanguage(); -}); \ No newline at end of file +}); + +function StartLogin() { + // disable all ui first + $("#ccn-login-form-login").attr("disabled",true); + $("#ccn-login-form-username").attr("disabled",true); + $("#ccn-login-form-password").attr("disabled",true); + + // get form data + username = $("#ccn-login-form-username").val(); + password = $("#ccn-login-form-password").val(); + + /* + // try get salt + if (cnn_api_common_salt(username)) { + // continue login + if (cnn_api_common_login(username, password)) { + // ok, logged + // jump into home page again + window.location.href = '/web/home'; + + } else alert($.i18n.prop("ccn-js-failToLogin")); + } else alert($.i18n.prop("ccn-js-failToLogin")); + */ + if (cnn_api_common_webLogin(username, password)) { + // ok, logged + // jump into home page again + window.location.href = '/web/home'; + return; + + } else alert($.i18n.prop("ccn-js-failToLogin")); + + // retore ui + $("#ccn-login-form-login").removeAttr("disabled"); + $("#ccn-login-form-username").removeAttr("disabled"); + $("#ccn-login-form-password").removeAttr("disabled"); +} diff --git a/src/static/js/utils.js b/src/static/js/utils.js new file mode 100644 index 0000000..7f82efc --- /dev/null +++ b/src/static/js/utils.js @@ -0,0 +1,36 @@ +function ComputPasswordWithSalt(password, salt) { + return ComputeSHA256(ComputeSHA256(password) + salt.toString()); +} + +function ComputeSHA256(strl) { + var tempstr = new TextEncoder().encode(strl); + var hashedStrl = undefined + var shitpromise = crypto.subtle.digest('SHA-256', tempstr); + Promise.all(shitpromise).then(function(result) { + hashedStrl = result; + }); + var hashArray = Array.from(new Uint8Array(hashedStrl)); + var hashHex = hashArray.map(b => ('00' + b.toString(16)).slice(-2)).join(''); + return hashHex.toLowerCase(); +} + +function IsResponseOK(data) { + if (typeof(data) == 'undefined') { + console.log("Fail to execute an api!"); + return false; + } + if (!data['success']) { + console.log("Fail to execute an api! Reason:"); + console.log(data['error']); + return false; + } + return true; +} + +function GetApiToken() { + return ccn_localstorageAssist_Get('ccn-token', ''); +} + +function SetApiToken(value) { + ccn_localstorageAssist_Set('ccn-token', value); +} diff --git a/src/templates/admin.html b/src/templates/admin.html index 5ef3456..bc139c6 100644 --- a/src/templates/admin.html +++ b/src/templates/admin.html @@ -16,6 +16,7 @@ + diff --git a/src/templates/calendar.html b/src/templates/calendar.html index 9bbd664..46b9dd7 100644 --- a/src/templates/calendar.html +++ b/src/templates/calendar.html @@ -17,6 +17,7 @@ + diff --git a/src/templates/home.html b/src/templates/home.html index 4018f28..608528a 100644 --- a/src/templates/home.html +++ b/src/templates/home.html @@ -15,6 +15,7 @@ + diff --git a/src/templates/login.html b/src/templates/login.html index acca2ca..444f648 100644 --- a/src/templates/login.html +++ b/src/templates/login.html @@ -15,6 +15,7 @@ + @@ -28,7 +29,7 @@