diff --git a/backend/coconut-leaf.py b/backend/coconut-leaf.py index 0d3a689..96d18b1 100644 --- a/backend/coconut-leaf.py +++ b/backend/coconut-leaf.py @@ -1,12 +1,14 @@ import sys -import logging from argparse import ArgumentParser from typing import cast from pathlib import Path + import server import config import utils import database +import logger +from logger import LOGGER, LoggerLevel def GetUsernamePassword() -> tuple[str, str]: @@ -26,15 +28,10 @@ def GetUsernamePassword() -> tuple[str, str]: return (username, password) - -def SetLoggingStyle(level: int) -> None: - logging.basicConfig(format="[%(levelname)s] %(message)s", level=level) - - if __name__ == "__main__": # Set as INFO level in default first, # and we will change it once we load the configuration file. - SetLoggingStyle(logging.INFO) + logger.set_level(LoggerLevel.INFO) # Receive arguments parser = ArgumentParser( @@ -60,21 +57,21 @@ if __name__ == "__main__": args = parser.parse_args() # Show splash - logging.info("Coconut-leaf") - logging.info("A light, self-host and multi-account calendar system") - logging.info("Project: https://github.com/yyc12345/coconut-leaf") - logging.info("===================") + LOGGER.info("Coconut-leaf") + LOGGER.info("A light, self-host and multi-account calendar system") + LOGGER.info("Project: https://github.com/yyc12345/coconut-leaf") + LOGGER.info("===================") # Load config file try: config.setup_config(cast(Path, args.config)) except Exception as e: - logging.critical(f"Error loading config file: {e}") + LOGGER.critical(f"Error loading config file: {e}") sys.exit(1) # Change logging level again according to whether enable debug mode - logging_level = logging.DEBUG if config.get_config().others.debug else logging.INFO - SetLoggingStyle(logging_level) + logging_level = LoggerLevel.DEBUG if config.get_config().others.debug else LoggerLevel.INFO + logger.set_level(logging_level) # Initialize the calendar system if needed if cast(bool, args.init): @@ -83,5 +80,5 @@ if __name__ == "__main__": calendar.init(*gotten_data) calendar.close() - logging.info("Staring server...") + LOGGER.info("Staring server...") server.run() diff --git a/backend/database.py b/backend/database.py index 6586e2b..7e3452b 100644 --- a/backend/database.py +++ b/backend/database.py @@ -1,11 +1,23 @@ -import config import sqlite3 -import utils import threading -import logging -import dt from typing import cast from pathlib import Path +from dataclasses import dataclass +from typing import Any + +import dt +import utils +import config +from logger import LOGGER + +@dataclass(frozen=True) +class ResponseBody: + success: bool + """True if this operation is successful, otherwise false.""" + error: str + """The error message provided when operation failed.""" + data: Any + """The payload provided when operation successed.""" def SafeDatabaseOperation(func): def wrapper(self: 'CalendarDatabase', *args, **kwargs): @@ -19,18 +31,18 @@ def SafeDatabaseOperation(func): except Exception as e: self.cursor = None if cfg.others.debug: - logging.exception(e) - return (False, str(e), None) + LOGGER.exception(e) + return ResponseBody(False, str(e), None) # do real data work try: currentTime = utils.GetCurrentTimestamp() if currentTime - self.latestClean > cfg.others.auto_token_clean_duration: self.latestClean = currentTime - logging.info('Cleaning outdated token...') + LOGGER.info('Cleaning outdated token...') self.tokenOper_clean() - result = (True, '', func(self, *args, **kwargs)) + result = ResponseBody(True, '', func(self, *args, **kwargs)) self.cursor.close() self.cursor = None self.db.commit() @@ -40,8 +52,8 @@ def SafeDatabaseOperation(func): self.cursor = None self.db.rollback() if cfg.others.debug: - logging.exception(e) - return (False, str(e), None) + LOGGER.exception(e) + return ResponseBody(False, str(e), None) return wrapper @@ -182,7 +194,12 @@ class CalendarDatabase: @SafeDatabaseOperation def common_webLogin(self, username, password, clientUa, clientIp): - self.cursor.execute('SELECT [name] FROM user WHERE [name] = ? AND [password] = ?;', (username, utils.ComputePasswordHash(password))) + LOGGER.debug(f'WebLogin Username: {username}') + LOGGER.debug(f'WebLogin Password: {password}') + passwordHash = utils.ComputePasswordHash(password) + LOGGER.debug(f'WebLogin Password Hash: {passwordHash}') + + self.cursor.execute('SELECT [name] FROM user WHERE [name] = ? AND [password] = ?;', (username, passwordHash)) if len(self.cursor.fetchall()) != 0: token = utils.GenerateToken(username) @@ -553,8 +570,8 @@ class CalendarDatabase: argumentsList.append(_username) self.cursor.execute('UPDATE user SET {} WHERE [name] = ?;'.format(', '.join(sqlList)), tuple(argumentsList)) - logging.debug(cache) - logging.debug(tuple(argumentsList)) + LOGGER.debug(cache) + LOGGER.debug(tuple(argumentsList)) if self.cursor.rowcount != 1: raise Exception('Fail to update due to no matched rows or too much rows.') return True diff --git a/backend/logger.py b/backend/logger.py new file mode 100644 index 0000000..a2ecd69 --- /dev/null +++ b/backend/logger.py @@ -0,0 +1,42 @@ +import logging +import enum + + +def _build_logger() -> tuple[logging.Logger, logging.Handler]: + # Create a new logger which is independent with Flask + logger = logging.getLogger("my_console_logger") + # Avoid message was propagated to root logger or captured by Flask logger. + logger.propagate = False + # Set initial level. + logger.setLevel(logging.INFO) + + # Create StreamHandler to output into stderr. + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.DEBUG) + # Set format for it. + formatter = logging.Formatter("[%(levelname)s] %(message)s") + console_handler.setFormatter(formatter) + # Add handler + logger.addHandler(console_handler) + + return (logger, console_handler) + + +(LOGGER, CONSOLE_HANDLER) = _build_logger() + + +class LoggerLevel(enum.IntEnum): + DEBUG = enum.auto() + INFO = enum.auto() + + +def set_level(level: LoggerLevel) -> None: + logging_level: int = logging.INFO + match level: + case LoggerLevel.DEBUG: + logging_level = logging.DEBUG + case LoggerLevel.INFO: + logging_level = logging.INFO + + LOGGER.setLevel(logging_level) + CONSOLE_HANDLER.setLevel(logging_level) diff --git a/backend/server.py b/backend/server.py index 6c6c9d0..56cecbf 100644 --- a/backend/server.py +++ b/backend/server.py @@ -1,8 +1,12 @@ from flask import Flask from flask import request +from dataclasses import dataclass +from typing import Any, Callable + import config import database import utils +from logger import LOGGER app = Flask(__name__) calendar_db = database.CalendarDatabase() @@ -14,7 +18,7 @@ calendar_db = database.CalendarDatabase() @app.route('/common/salt', methods=['POST']) def api_common_saltHandle(): return SmartDbCaller(calendar_db.common_salt, - (('username', str, False), ), + (FormField('username', str, False), ), None) @app.route('/common/login', methods=['POST']) @@ -27,10 +31,10 @@ def api_common_loginHandle(): clientIp = request.remote_addr return SmartDbCaller(calendar_db.common_login, - (('username', str, False), - ('password', str, False), - ('clientUa', str, False), - ('clientIp', str, False)), + (FormField('username', str, False), + FormField('password', str, False), + FormField('clientUa', str, False), + FormField('clientIp', str, False)), { 'clientUa': clientUa, 'clientIp': clientIp @@ -44,12 +48,12 @@ def api_common_webLoginHandle(): 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), - ('clientUa', str, False), - ('clientIp', str, False)), + (FormField('username', str, False), + FormField('password', str, False), + FormField('clientUa', str, False), + FormField('clientIp', str, False)), { 'clientUa': clientUa, 'clientIp': clientIp @@ -58,13 +62,13 @@ def api_common_webLoginHandle(): @app.route('/common/logout', methods=['POST']) def api_common_logoutHandle(): return SmartDbCaller(calendar_db.common_logout, - (('token', str, False), ), + (FormField('token', str, False), ), None) @app.route('/common/tokenValid', methods=['POST']) def api_common_tokenValidHandle(): return SmartDbCaller(calendar_db.common_tokenValid, - (('token', str, False), ), + (FormField('token', str, False), ), None) # endregion @@ -74,60 +78,60 @@ def api_common_tokenValidHandle(): @app.route('/calendar/getFull', methods=['POST']) def api_calendar_getFullHandle(): return SmartDbCaller(calendar_db.calendar_getFull, - (('token', str, False), - ('startDateTime', int, False), - ('endDateTime', int, False)), + (FormField('token', str, False), + FormField('startDateTime', int, False), + FormField('endDateTime', int, False)), None) @app.route('/calendar/getList', methods=['POST']) def api_calendar_getListHandle(): return SmartDbCaller(calendar_db.calendar_getList, - (('token', str, False), - ('startDateTime', int, False), - ('endDateTime', int, False)), + (FormField('token', str, False), + FormField('startDateTime', int, False), + FormField('endDateTime', int, False)), None) @app.route('/calendar/getDetail', methods=['POST']) def api_calendar_getDetailHandle(): return SmartDbCaller(calendar_db.calendar_getDetail, - (('token', str, False), - ('uuid', str, False)), + (FormField('token', str, False), + FormField('uuid', str, False)), None) @app.route('/calendar/update', methods=['POST']) def api_calendar_updateHandle(): return SmartDbCaller(calendar_db.calendar_update, - (('token', str, False), - ('uuid', str, False), - ('belongTo', str, True), - ('title', str, True), - ('description', str, True), - ('eventDateTimeStart', int, True), - ('eventDateTimeEnd', int, True), - ('loopRules', str, True), - ('timezoneOffset', int, True), - ('lastChange', str, False)), + (FormField('token', str, False), + FormField('uuid', str, False), + FormField('belongTo', str, True), + FormField('title', str, True), + FormField('description', str, True), + FormField('eventDateTimeStart', int, True), + FormField('eventDateTimeEnd', int, True), + FormField('loopRules', str, True), + FormField('timezoneOffset', int, True), + FormField('lastChange', str, False)), None) @app.route('/calendar/add', methods=['POST']) def api_calendar_addHandle(): return SmartDbCaller(calendar_db.calendar_add, - (('token', str, False), - ('belongTo', str, False), - ('title', str, False), - ('description', str, False), - ('eventDateTimeStart', int, False), - ('eventDateTimeEnd', int, False), - ('loopRules', str, False), - ('timezoneOffset', int, False)), + (FormField('token', str, False), + FormField('belongTo', str, False), + FormField('title', str, False), + FormField('description', str, False), + FormField('eventDateTimeStart', int, False), + FormField('eventDateTimeEnd', int, False), + FormField('loopRules', str, False), + FormField('timezoneOffset', int, False)), None) @app.route('/calendar/delete', methods=['POST']) def api_calendar_deleteHandle(): return SmartDbCaller(calendar_db.calendar_delete, - (('token', str, False), - ('uuid', str, False), - ('lastChange', str, False)), + (FormField('token', str, False), + FormField('uuid', str, False), + FormField('lastChange', str, False)), None) # endregion @@ -137,77 +141,77 @@ def api_calendar_deleteHandle(): @app.route('/collection/getFullOwn', methods=['POST']) def api_collection_getFullOwnHandle(): return SmartDbCaller(calendar_db.collection_getFullOwn, - (('token', str, False), ), + (FormField('token', str, False), ), None) @app.route('/collection/getListOwn', methods=['POST']) def api_collection_getListOwnHandle(): return SmartDbCaller(calendar_db.collection_getListOwn, - (('token', str, False), ), + (FormField('token', str, False), ), None) @app.route('/collection/getDetailOwn', methods=['POST']) def api_collection_getDetailOwnHandle(): return SmartDbCaller(calendar_db.collection_getDetailOwn, - (('token', str, False), - ('uuid', str, False)), + (FormField('token', str, False), + FormField('uuid', str, False)), None) @app.route('/collection/addOwn', methods=['POST']) def api_collection_addOwnHandle(): return SmartDbCaller(calendar_db.collection_addOwn, - (('token', str, False), - ('name', str, False)), + (FormField('token', str, False), + FormField('name', str, False)), None) @app.route('/collection/updateOwn', methods=['POST']) def api_collection_updateOwnHandle(): return SmartDbCaller(calendar_db.collection_updateOwn, - (('token', str, False), - ('uuid', str, False), - ('name', str, False), - ('lastChange', str, False)), + (FormField('token', str, False), + FormField('uuid', str, False), + FormField('name', str, False), + FormField('lastChange', str, False)), None) @app.route('/collection/deleteOwn', methods=['POST']) def api_collection_deleteOwnHandle(): return SmartDbCaller(calendar_db.collection_deleteOwn, - (('token', str, False), - ('uuid', str, False), - ('lastChange', str, False)), + (FormField('token', str, False), + FormField('uuid', str, False), + FormField('lastChange', str, False)), None) @app.route('/collection/getSharing', methods=['POST']) def api_collection_getSharingHandle(): return SmartDbCaller(calendar_db.collection_getSharing, - (('token', str, False), - ('uuid', str, False)), + (FormField('token', str, False), + FormField('uuid', str, False)), None) @app.route('/collection/deleteSharing', methods=['POST']) def api_collection_deleteSharingHandle(): return SmartDbCaller(calendar_db.collection_deleteSharing, - (('token', str, False), - ('uuid', str, False), - ('target', str, False), - ('lastChange', str, False)), + (FormField('token', str, False), + FormField('uuid', str, False), + FormField('target', str, False), + FormField('lastChange', str, False)), None) @app.route('/collection/addSharing', methods=['POST']) def api_collection_addSharingHandle(): return SmartDbCaller(calendar_db.collection_addSharing, - (('token', str, False), - ('uuid', str, False), - ('target', str, False), - ('lastChange', str, False)), + (FormField('token', str, False), + FormField('uuid', str, False), + FormField('target', str, False), + FormField('lastChange', str, False)), None) @app.route('/collection/getShared', methods=['POST']) def api_collection_getSharedHandle(): return SmartDbCaller(calendar_db.collection_getShared, - (('token', str, False), ), + (FormField('token', str, False), ), None) # endregion @@ -217,43 +221,43 @@ def api_collection_getSharedHandle(): @app.route('/todo/getFull', methods=['POST']) def api_todo_getFullHandle(): return SmartDbCaller(calendar_db.todo_getFull, - (('token', str, False), ), + (FormField('token', str, False), ), None) @app.route('/todo/getList', methods=['POST']) def api_todo_getListHandle(): return SmartDbCaller(calendar_db.todo_getList, - (('token', str, False), ), + (FormField('token', str, False), ), None) @app.route('/todo/getDetail', methods=['POST']) def api_todo_getDetailHandle(): return SmartDbCaller(calendar_db.todo_getDetail, - (('token', str, False), - ('uuid', str, False)), + (FormField('token', str, False), + FormField('uuid', str, False)), None) @app.route('/todo/add', methods=['POST']) def api_todo_addHandle(): return SmartDbCaller(calendar_db.todo_add, - (('token', str, False), ), + (FormField('token', str, False), ), None) @app.route('/todo/update', methods=['POST']) def api_todo_updateHandle(): return SmartDbCaller(calendar_db.todo_update, - (('token', str, False), - ('uuid', str, False), - ('data', str, False), - ('lastChange', str, False)), + (FormField('token', str, False), + FormField('uuid', str, False), + FormField('data', str, False), + FormField('lastChange', str, False)), None) @app.route('/todo/delete', methods=['POST']) def api_todo_deleteHandle(): return SmartDbCaller(calendar_db.todo_delete, - (('token', str, False), - ('uuid', str, False), - ('lastChange', str, False)), + (FormField('token', str, False), + FormField('uuid', str, False), + FormField('lastChange', str, False)), None) # endregion @@ -263,30 +267,30 @@ def api_todo_deleteHandle(): @app.route('/admin/get', methods=['POST']) def api_admin_getHandle(): return SmartDbCaller(calendar_db.admin_get, - (('token', str, False), ), + (FormField('token', str, False), ), None) @app.route('/admin/add', methods=['POST']) def api_admin_addHandle(): return SmartDbCaller(calendar_db.admin_add, - (('token', str, False), - ('username', str, False)), + (FormField('token', str, False), + FormField('username', str, False)), None) @app.route('/admin/update', methods=['POST']) def api_admin_updateHandle(): return SmartDbCaller(calendar_db.admin_update, - (('token', str, False), - ('username', str, False), - ('password', str, True), - ('isAdmin', utils.Str2Bool, True)), + (FormField('token', str, False), + FormField('username', str, False), + FormField('password', str, True), + FormField('isAdmin', utils.Str2Bool, True)), None) @app.route('/admin/delete', methods=['POST']) def api_admin_deleteHandle(): return SmartDbCaller(calendar_db.admin_delete, - (('token', str, False), - ('username', str, False)), + (FormField('token', str, False), + FormField('username', str, False)), None) # endregion @@ -296,27 +300,27 @@ def api_admin_deleteHandle(): @app.route('/profile/isAdmin', methods=['POST']) def api_profile_isAdminHandle(): return SmartDbCaller(calendar_db.profile_isAdmin, - (('token', str, False), ), + (FormField('token', str, False), ), None) @app.route('/profile/changePassword', methods=['POST']) def api_profile_changePasswordHandle(): return SmartDbCaller(calendar_db.profile_changePassword, - (('token', str, False), - ('password', str, False)), + (FormField('token', str, False), + FormField('password', str, False)), None) @app.route('/profile/getToken', methods=['POST']) def api_profile_getTokenHandle(): return SmartDbCaller(calendar_db.profile_getToken, - (('token', str, False), ), + (FormField('token', str, False), ), None) @app.route('/profile/deleteToken', methods=['POST']) def api_profile_deleteTokenHandle(): return SmartDbCaller(calendar_db.profile_deleteToken, - (('token', str, False), - ('deleteToken', str, False)), + (FormField('token', str, False), + FormField('deleteToken', str, False)), None) # endregion @@ -325,41 +329,64 @@ def api_profile_deleteTokenHandle(): # region: Misc Functions -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 - realForm = request.form.to_dict() - if extraDict is not None: - realForm.update(extraDict) - for item in paramTuple: - cache = item[1](realForm.get(item[0], None)) - if item[2]: - # optional param - if cache is not None: - optParamDict[item[0]] = cache - optCount += 1 - else: - if cache is None: - break - paramList.append(cache) - else: - # at least one opt param - if optCount == 0 or len(optParamDict) != 0: - result = dbMethod(*paramList, **optParamDict) +@dataclass(frozen=True) +class FormField: + name: str + """The name of form field.""" + ty: Callable[[str], Any] + """The type of form field.""" + is_optional: bool + """True if this form field is optional, otherwise false.""" + +def SmartDbCaller(db_method: Callable, fields: tuple[FormField, ...], padding_form: dict[str, Any] | None) -> dict[str, Any]: + result = ResponseBody(False, 'Invalid parameter', None) + opt_param_counter = 0 + param_list: list[Any] = [] + opt_param_dict: dict[str, Any] = {} + real_form = request.form.to_dict() + LOGGER.debug(f'Form: {real_form}') + if padding_form is not None: + real_form.update(padding_form) + + for field in fields: + value = real_form.get(field.name, None) + if value is not None: + value = field.ty(value) + + if field.is_optional: + # optional param + if value is not None: + opt_param_dict[field.name] = value + opt_param_counter += 1 + else: + # required param + if value is None: + break + param_list.append(value) + + # at least one opt param + LOGGER.debug(f'All Optional Parameter: {opt_param_counter}') + LOGGER.debug(f'Optional Parameter Count: {len(opt_param_dict)}') + if opt_param_counter == 0 or len(opt_param_dict) != 0: + result: ResponseBody = db_method(*param_list, **opt_param_dict) + return ConstructResponseBody(result) -def ConstructResponseBody(returnedTuple): +@dataclass(frozen=True) +class ResponseBody: + success: bool + """True if this operation is successful, otherwise false.""" + error: str + """The error message provided when operation failed.""" + data: Any + """The payload provided when operation successed.""" + +def ConstructResponseBody(body: ResponseBody) -> dict[str, Any]: return { - 'success': returnedTuple[0], - 'error': returnedTuple[1], - 'data': returnedTuple[2] + 'success': body.success, + 'error': body.error, + 'data': body.data } def run(): diff --git a/frontend/package.json b/frontend/package.json index 9b396f0..ae7a8c7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "@fortawesome/fontawesome-svg-core": "^7.2.0", "@fortawesome/free-solid-svg-icons": "^7.2.0", "@fortawesome/vue-fontawesome": "^3.2.0", + "axios": "1.14.0", "bulma": "0.9.1", "pinia": "^3.0.4", "pinia-plugin-persistedstate": "^4.7.1", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 73047e3..7e86da7 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@fortawesome/vue-fontawesome': specifier: ^3.2.0 version: 3.2.0(@fortawesome/fontawesome-svg-core@7.2.0)(vue@3.5.33(typescript@6.0.3)) + axios: + specifier: 1.14.0 + version: 1.14.0 bulma: specifier: 0.9.1 version: 0.9.1 @@ -876,6 +879,12 @@ packages: resolution: {integrity: sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==} engines: {node: '>=20.19.0'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.14.0: + resolution: {integrity: sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==} + balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} @@ -911,6 +920,10 @@ packages: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + caniuse-lite@1.0.30001791: resolution: {integrity: sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==} @@ -922,6 +935,10 @@ packages: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -974,10 +991,18 @@ packages: defu@6.1.7: resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + electron-to-chromium@1.5.344: resolution: {integrity: sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==} @@ -988,6 +1013,22 @@ packages: error-stack-parser-es@1.0.5: resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -1107,15 +1148,39 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1124,6 +1189,22 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} @@ -1314,6 +1395,10 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + memorystream@0.3.1: resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} engines: {node: '>= 0.10.0'} @@ -1326,6 +1411,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} @@ -1478,6 +1571,10 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2572,6 +2669,16 @@ snapshots: '@babel/parser': 7.29.2 ast-kit: 2.2.0 + asynckit@0.4.0: {} + + axios@1.14.0: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.5 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + balanced-match@4.0.4: {} baseline-browser-mapping@2.10.23: {} @@ -2602,6 +2709,11 @@ snapshots: dependencies: run-applescript: 7.1.0 + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + caniuse-lite@1.0.30001791: {} chokidar@4.0.3: @@ -2613,6 +2725,10 @@ snapshots: dependencies: readdirp: 5.0.0 + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + confbox@0.1.8: {} confbox@0.2.4: {} @@ -2650,14 +2766,37 @@ snapshots: defu@6.1.7: {} + delayed-stream@1.0.0: {} + detect-libc@2.1.2: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + electron-to-chromium@1.5.344: {} entities@7.0.1: {} error-stack-parser-es@1.0.5: {} + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.3 + escalade@3.2.0: {} escape-string-regexp@4.0.0: {} @@ -2792,11 +2931,41 @@ snapshots: flatted@3.4.2: {} + follow-redirects@1.16.0: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.3 + mime-types: 2.1.35 + fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + gensync@1.0.0-beta.2: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.3 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -2805,6 +2974,18 @@ snapshots: dependencies: is-glob: 4.0.3 + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + hookable@5.5.3: {} ignore@5.3.2: {} @@ -2940,6 +3121,8 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + math-intrinsics@1.1.0: {} + memorystream@0.3.1: {} merge2@1.4.1: {} @@ -2949,6 +3132,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.2 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + minimatch@10.2.5: dependencies: brace-expansion: 5.0.5 @@ -3100,6 +3289,8 @@ snapshots: prelude-ls@1.2.1: {} + proxy-from-env@2.1.0: {} + punycode@2.3.1: {} quansync@0.2.11: {} diff --git a/frontend/src/api/admin.ts b/frontend/src/api/admin.ts new file mode 100644 index 0000000..cf52dc6 --- /dev/null +++ b/frontend/src/api/admin.ts @@ -0,0 +1,75 @@ +import { apiWrapper, boolApiWrapper } from './index' + +// User interface +interface User { + username: string + isAdmin: boolean + // Add other user-related fields as needed +} + +/** + * Get all users (admin only) + * @param token - Authentication token + * @returns Array of users or null if operation failed + */ +export async function get(token: string): Promise { + return apiWrapper('/api/admin/get', { token }); +} + +/** + * Add new user (admin only) + * @param token - Authentication token + * @param username - Username + * @returns Created user or null if operation failed + */ +export async function add(token: string, username: string): Promise { + return apiWrapper('/api/admin/add', { token, username }); +} + +/** + * Update user (admin only) + * @param token - Authentication token + * @param username - Username + * @param params - Update parameters (partial) + * @returns true if update successful, false otherwise + */ +export async function update( + token: string, + username: string, + params: { + password?: string + isAdmin?: boolean + } +): Promise { + const data: any = { + token, + username + }; + + let count = 0; + if (typeof params.password !== 'undefined') { + data.password = params.password; + count++; + } + if (typeof params.isAdmin !== 'undefined') { + data.isAdmin = params.isAdmin; + count++; + } + + // If no update parameters provided, return true + if (count === 0) { + return true; + } + + return boolApiWrapper('/api/admin/update', data); +} + +/** + * Delete user (admin only) + * @param token - Authentication token + * @param username - Username + * @returns true if deletion successful, false otherwise + */ +export async function deleteItem(token: string, username: string): Promise { + return boolApiWrapper('/api/admin/delete', { token, username }); +} diff --git a/frontend/src/api/calendar.ts b/frontend/src/api/calendar.ts new file mode 100644 index 0000000..a864a85 --- /dev/null +++ b/frontend/src/api/calendar.ts @@ -0,0 +1,141 @@ +import { apiWrapper, boolApiWrapper } from './index' + +// Calendar event interface +interface CalendarEvent { + uuid: string + belongTo: string + title: string + description: string + eventDateTimeStart: string + eventDateTimeEnd: string + loopRules: string + timezoneOffset: number + lastChange: string +} + +// Description object interface +interface DescriptionObject { + description: string + color: string +} + +/** + * Serialize calendar description + * @param description - Description text + * @param color - Color value + * @returns JSON string + */ +export function serializeDescription(description: string, color: string): string { + const sobj: DescriptionObject = { + description, + color + } + return JSON.stringify(sobj) +} + +/** + * Deserialize calendar description + * @param str - JSON string + * @returns Description object + */ +export function deserializeDescription(str: string): DescriptionObject { + try { + return JSON.parse(str) as DescriptionObject + } catch (err) { + return { + description: "", + color: "#000000" // DefaultColor + } + } +} + +/** + * Get full calendar events within date range + * @param token - Authentication token + * @param startDateTime - Start datetime + * @param endDateTime - End datetime + * @returns Array of calendar events or null if operation failed + */ +export async function getFull(token: string, startDateTime: string, endDateTime: string): Promise { + return apiWrapper('/api/calendar/getFull', { token, startDateTime, endDateTime }); +} + +/** + * Get calendar event detail by UUID + * @param token - Authentication token + * @param uuid - Event UUID + * @returns Calendar event or null if operation failed + */ +export async function getDetail(token: string, uuid: string): Promise { + return apiWrapper('/api/calendar/getDetail', { token, uuid }); +} + +/** + * Update calendar event + * @param token - Authentication token + * @param uuid - Event UUID + * @param params - Update parameters (partial) + * @returns Updated calendar event or null if operation failed + */ +export async function update( + token: string, + uuid: string, + params: { + belongTo?: string + title?: string + description?: string + eventDateTimeStart?: string + eventDateTimeEnd?: string + loopRules?: string + timezoneOffset?: number + lastChange: string + } +): Promise { + const data: any = { + token, + uuid, + lastChange: params.lastChange + }; + + if (params.belongTo !== undefined) data.belongTo = params.belongTo; + if (params.title !== undefined) data.title = params.title; + if (params.description !== undefined) data.description = params.description; + if (params.eventDateTimeStart !== undefined) data.eventDateTimeStart = params.eventDateTimeStart; + if (params.eventDateTimeEnd !== undefined) data.eventDateTimeEnd = params.eventDateTimeEnd; + if (params.loopRules !== undefined) data.loopRules = params.loopRules; + if (params.timezoneOffset !== undefined) data.timezoneOffset = params.timezoneOffset; + + return apiWrapper('/api/calendar/update', data); +} + +/** + * Add new calendar event + * @param token - Authentication token + * @param params - Event parameters + * @returns Created calendar event or null if operation failed + */ +export async function add( + token: string, + params: { + belongTo: string + title: string + description: string + eventDateTimeStart: string + eventDateTimeEnd: string + loopRules: string + timezoneOffset: number + } +): Promise { + return apiWrapper('/api/calendar/add', { token, ...params }); +} + +/** + * Delete calendar event + * @param token - Authentication token + * @param uuid - Event UUID + * @param lastChange - Last change timestamp + * @returns true if deletion successful, false otherwise + */ +export async function deleteEvent(token: string, uuid: string, lastChange: string): Promise { + return boolApiWrapper('/api/calendar/delete', { token, uuid, lastChange }); +} diff --git a/frontend/src/api/collection.ts b/frontend/src/api/collection.ts new file mode 100644 index 0000000..0b4e1c4 --- /dev/null +++ b/frontend/src/api/collection.ts @@ -0,0 +1,126 @@ +import { apiWrapper, boolApiWrapper } from './index' + +type Uuid = string; + +// Collection item interface +interface CollectionItem { + uuid: Uuid + name: string + lastChange: string +} + +// Sharing info interface +interface SharingInfo { + target: string + // Add other sharing-related fields as needed +} + +/** + * Get all owned collections + * @param token - Authentication token + * @returns Array of collection items or null if operation failed + */ +export async function getFullOwn(token: string): Promise { + return apiWrapper('/api/collection/getFullOwn', { token }); +} + +/** + * Get owned collection detail by UUID + * @param token - Authentication token + * @param uuid - Collection UUID + * @returns Collection item or null if operation failed + */ +export async function getDetailOwn(token: string, uuid: Uuid): Promise { + return apiWrapper('/api/collection/getDetailOwn', { token, uuid }); +} + +/** + * Add new owned collection + * @param token - Authentication token + * @param name - Collection name + * @returns Created collection item or null if operation failed + */ +export async function addOwn(token: string, name: string): Promise { + return apiWrapper('/api/collection/addOwn', { token, name }); +} + +/** + * Update owned collection + * @param token - Authentication token + * @param uuid - Collection UUID + * @param name - New name + * @param lastChange - Last change timestamp + * @returns Updated collection item or null if operation failed + */ +export async function updateOwn( + token: string, + uuid: Uuid, + name: string, + lastChange: string +): Promise { + return apiWrapper('/api/collection/updateOwn', { token, uuid, name, lastChange }); +} + +/** + * Delete owned collection + * @param token - Authentication token + * @param uuid - Collection UUID + * @param lastChange - Last change timestamp + * @returns true if deletion successful, false otherwise + */ +export async function deleteOwn(token: string, uuid: Uuid, lastChange: string): Promise { + return boolApiWrapper('/api/collection/deleteOwn', { token, uuid, lastChange }); +} + +/** + * Get sharing information for a collection + * @param token - Authentication token + * @param uuid - Collection UUID + * @returns Array of sharing info or null if operation failed + */ +export async function getSharing(token: string, uuid: Uuid): Promise { + return apiWrapper('/api/collection/getSharing', { token, uuid }); +} + +/** + * Delete sharing for a collection + * @param token - Authentication token + * @param uuid - Collection UUID + * @param target - Target user + * @param lastChange - Last change timestamp + * @returns Result data or null if operation failed + */ +export async function deleteSharing( + token: string, + uuid: Uuid, + target: string, + lastChange: string +): Promise { + return apiWrapper('/api/collection/deleteSharing', { token, uuid, target, lastChange }); +} + +/** + * Add sharing for a collection + * @param token - Authentication token + * @param uuid - Collection UUID + * @param target - Target user + * @param lastChange - Last change timestamp + * @returns Result data or null if operation failed + */ +export async function addSharing( + token: string, + uuid: Uuid, + target: string, + lastChange: string +): Promise { + return apiWrapper('/api/collection/addSharing', { token, uuid, target, lastChange }); +} + +/** + * Get all shared collections + * @param token - Authentication token + * @returns Array of collection items or null if operation failed + */ +export async function getShared(token: string): Promise { + return apiWrapper('/api/collection/getShared', { token }); +} diff --git a/frontend/src/api/common.ts b/frontend/src/api/common.ts new file mode 100644 index 0000000..79f3a34 --- /dev/null +++ b/frontend/src/api/common.ts @@ -0,0 +1,43 @@ +import { apiWrapper, boolApiWrapper } from './index' + +// /** +// * Login with salt +// * @param username - Username +// * @param password - Password +// * @returns Token if login successful, undefined otherwise +// */ +// export async function login(username: string, password: string): Promise { +// const salt: string | undefined = await apiWrapper('/api/common/salt', { username }); +// if (typeof salt === 'undefined') return undefined; + +// const computedPassword = computePasswordWithSalt(password, salt); +// return apiWrapper('/api/common/login', { username, password: computedPassword }); +// } + +/** + * Web login + * @param username - Username + * @param password - Password + * @returns Token if login successful, undefined otherwise + */ +export async function webLogin(username: string, password: string): Promise { + return apiWrapper('/api/common/webLogin', { username, password }); +} + +/** + * Logout + * @param token - Authentication token + * @returns true if logout successful, false otherwise if logout failed + */ +export async function logout(token: string): Promise { + return boolApiWrapper('/api/common/logout', { token }); +} + +/** + * Validate token + * @param token - Authentication token + * @returns true if token is valid, false otherwise + */ +export async function tokenValid(token: string): Promise { + return boolApiWrapper('/api/common/tokenValid', { token }); +} diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts new file mode 100644 index 0000000..74f2897 --- /dev/null +++ b/frontend/src/api/index.ts @@ -0,0 +1,57 @@ +// Response interface +interface ApiResponse { + success: boolean, + error: string, + data: T, +} + +export async function apiWrapper(url: string, data: Record): Promise { + try { + // 自动编码为 key=value&key2=value2 格式 + const params = new URLSearchParams(); + Object.entries(data).forEach(([key, value]) => { + params.append(key, String(value)); + }); + // 发起请求 + const response = await fetch(url, { + method: "POST", + mode: "cors", + cache: "no-cache", + credentials: "same-origin", + headers: { + // 明确指定内容类型 + 'Content-Type': 'application/x-www-form-urlencoded', + }, + redirect: "follow", + referrerPolicy: "no-referrer", + body: params.toString(), + }); + + // 检查 HTTP 状态码 (fetch 只有在网络故障时才会 reject,HTTP 404/500 不会) + if (!response.ok) { + console.error(`HTTP failed: ${response.status}`); + } + + // 解析 JSON body + // 注意:response.json() 返回的是一个 Promise,所以需要 await + const payload = await response.json() as ApiResponse; + + // 检查API返回结果 + if (payload.success) { + return payload.data; + } else { + console.error(`API failed: ${payload.error}`); + return undefined; + } + } catch (error) { + // 统一错误处理 + console.error(`Fetch failed: ${error}`); + return undefined; + } +} + +export async function boolApiWrapper(url: string, data: Record): Promise { + const rv = await apiWrapper(url, data); + return rv !== undefined; +} + diff --git a/frontend/src/api/profile.ts b/frontend/src/api/profile.ts new file mode 100644 index 0000000..3c4ffbe --- /dev/null +++ b/frontend/src/api/profile.ts @@ -0,0 +1,45 @@ +import { apiWrapper, boolApiWrapper } from './index' + +// Token info interface +interface TokenInfo { + token: string + // Add other token-related fields as needed +} + +/** + * Check if current user is admin + * @param token - Authentication token + * @returns true if user is admin, false otherwise + */ +export async function isAdmin(token: string): Promise { + return boolApiWrapper('/api/profile/isAdmin', { token }); +} + +/** + * Change user password + * @param token - Authentication token + * @param password - New password + * @returns true if change successful, false otherwise + */ +export async function changePassword(token: string, password: string): Promise { + return boolApiWrapper('/api/profile/changePassword', { token, password }); +} + +/** + * Get user tokens + * @param token - Authentication token + * @returns Array of token info or undefined if operation failed + */ +export async function getToken(token: string): Promise { + return apiWrapper('/api/profile/getToken', { token }); +} + +/** + * Delete a token + * @param token - Authentication token + * @param deleteToken - Token to delete + * @returns true if deletion successful, false otherwise + */ +export async function deleteToken(token: string, deleteToken: string): Promise { + return boolApiWrapper('/api/profile/deleteToken', { token, deleteToken }); +} diff --git a/frontend/src/api/todo.ts b/frontend/src/api/todo.ts new file mode 100644 index 0000000..e6b8ea9 --- /dev/null +++ b/frontend/src/api/todo.ts @@ -0,0 +1,56 @@ +import axios from 'axios' +import { apiWrapper, boolApiWrapper } from './index' + +// Todo item interface +interface TodoItem { + uuid: string + data: any + lastChange: string + // Add other todo-related fields as needed +} + +/** + * Get all todos + * @param token - Authentication token + * @returns Array of todo items or null if operation failed + */ +export async function getFull(token: string): Promise { + return apiWrapper('/api/todo/getFull', { token }); +} + +/** + * Add new todo + * @param token - Authentication token + * @returns Created todo item or null if operation failed + */ +export async function add(token: string): Promise { + return apiWrapper('/api/todo/add', { token }); +} + +/** + * Update todo + * @param token - Authentication token + * @param uuid - Todo UUID + * @param data - Todo data + * @param lastChange - Last change timestamp + * @returns Updated todo item or null if operation failed + */ +export async function update( + token: string, + uuid: string, + data: any, + lastChange: string +): Promise { + return apiWrapper('/api/todo/update', { token, uuid, data, lastChange }); +} + +/** + * Delete todo + * @param token - Authentication token + * @param uuid - Todo UUID + * @param lastChange - Last change timestamp + * @returns true if deletion successful, false otherwise + */ +export async function deleteTodo(token: string, uuid: string, lastChange: string): Promise { + return boolApiWrapper('/api/todo/delete', { token, uuid, lastChange }); +} diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 807f244..43210d2 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -48,4 +48,8 @@ router.beforeEach((to, from) => { } }) +export const goToHome = () => { + router.push({ name: 'Home' }) +} + export default router diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue index 441117b..27b59fb 100644 --- a/frontend/src/views/Login.vue +++ b/frontend/src/views/Login.vue @@ -1,11 +1,45 @@ @@ -16,7 +50,7 @@ const login = () => {
- + @@ -25,7 +59,7 @@ const login = () => {

- + @@ -33,7 +67,7 @@ const login = () => {

- +