1
0

feat: support login

- migrate old API into typescript but not finished (only webLogin works now)
- seperate the logger of backend due to the shitty behavior of Flask (change logging level)
This commit is contained in:
2026-05-14 21:13:13 +08:00
parent 6337ae432d
commit 2a280dcba0
15 changed files with 1013 additions and 157 deletions

View File

@@ -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()

View File

@@ -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

42
backend/logger.py Normal file
View File

@@ -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)

View File

@@ -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
@@ -46,10 +50,10 @@ def api_common_webLoginHandle():
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]:
@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 cache is not None:
optParamDict[item[0]] = cache
optCount += 1
if value is not None:
opt_param_dict[field.name] = value
opt_param_counter += 1
else:
if cache is None:
# required param
if value is None:
break
paramList.append(cache)
else:
# at least one opt param
if optCount == 0 or len(optParamDict) != 0:
result = dbMethod(*paramList, **optParamDict)
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():

View File

@@ -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",

191
frontend/pnpm-lock.yaml generated
View File

@@ -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: {}

75
frontend/src/api/admin.ts Normal file
View File

@@ -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<User[] | undefined> {
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<User | undefined> {
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<boolean> {
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<boolean> {
return boolApiWrapper('/api/admin/delete', { token, username });
}

View File

@@ -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<CalendarEvent[] | undefined> {
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<CalendarEvent | undefined> {
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<CalendarEvent | undefined> {
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<CalendarEvent | undefined> {
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<boolean> {
return boolApiWrapper('/api/calendar/delete', { token, uuid, lastChange });
}

View File

@@ -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<CollectionItem[] | undefined> {
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<CollectionItem | undefined> {
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<Uuid | undefined> {
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<CollectionItem | undefined> {
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<boolean> {
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<SharingInfo[] | undefined> {
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<any | undefined> {
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<any | undefined> {
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<CollectionItem[] | undefined> {
return apiWrapper('/api/collection/getShared', { token });
}

View File

@@ -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<string | undefined> {
// 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<string | undefined> {
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<boolean> {
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<boolean> {
return boolApiWrapper('/api/common/tokenValid', { token });
}

57
frontend/src/api/index.ts Normal file
View File

@@ -0,0 +1,57 @@
// Response interface
interface ApiResponse<T = any> {
success: boolean,
error: string,
data: T,
}
export async function apiWrapper<T>(url: string, data: Record<string, any>): Promise<T | undefined> {
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 只有在网络故障时才会 rejectHTTP 404/500 不会)
if (!response.ok) {
console.error(`HTTP failed: ${response.status}`);
}
// 解析 JSON body
// 注意response.json() 返回的是一个 Promise所以需要 await
const payload = await response.json() as ApiResponse<T>;
// 检查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<U>(url: string, data: Record<string, any>): Promise<boolean> {
const rv = await apiWrapper<null>(url, data);
return rv !== undefined;
}

View File

@@ -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<boolean> {
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<boolean> {
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<TokenInfo[] | undefined> {
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<boolean> {
return boolApiWrapper('/api/profile/deleteToken', { token, deleteToken });
}

56
frontend/src/api/todo.ts Normal file
View File

@@ -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<TodoItem[] | undefined> {
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<TodoItem | undefined> {
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<TodoItem | undefined> {
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<boolean> {
return boolApiWrapper('/api/todo/delete', { token, uuid, lastChange });
}

View File

@@ -48,4 +48,8 @@ router.beforeEach((to, from) => {
}
})
export const goToHome = () => {
router.push({ name: 'Home' })
}
export default router

View File

@@ -1,11 +1,45 @@
<script setup lang="ts">
import MessageBox from '@/components/MessageBox.vue';
import { ref } from 'vue';
import MessageBox from '@/components/MessageBox.vue';
import { useTokenStore } from '@/stores/token';
import { webLogin as apiCommonWebLogin } from '@/api/common';
import { goToHome } from '@/router';
const isLoggingIn = ref<boolean>(false);
const username = ref<string>("");
const password = ref<string>("");
const messagebox = ref<InstanceType<typeof MessageBox> | null>(null);
const login = () => {
messagebox.value?.show("Fail to login. Please check your username or password.")
const login = async () => {
// disable UI first
isLoggingIn.value = true;
// // try get salt
// if (ccn_api_common_salt(username)) {
// // continue login
// if (ccn_api_common_login(username, password)) {
// // ok, logged
// // jump into home page again
// window.location.href = '/web/home';
// } else ccn_messagebox_Show($.i18n.prop("ccn-i18n-js-fail-login"));
// } else ccn_messagebox_Show($.i18n.prop("ccn-i18n-js-fail-login"));
const token = await apiCommonWebLogin(username.value, password.value);
if (typeof token !== 'undefined') {
// OK. We have logged in.
// Update token storage
const tokenStore = useTokenStore();
tokenStore.login(token);
// Go to home page.
goToHome();
} else {
// Show login error.
messagebox.value?.show("Fail to login. Please check your username or password.");
}
// Enable all UI
isLoggingIn.value = false;
}
</script>
@@ -16,7 +50,7 @@ const login = () => {
<div class="field">
<label class="label">User Name</label>
<div class="control has-icons-left has-icons-right">
<input id="ccn-login-form-username" class="input" type="text">
<input v-model="username" :disabled="isLoggingIn" class="input" type="text">
<span class="icon is-small is-left">
<font-awesome-icon icon="fas fa-user"></font-awesome-icon>
</span>
@@ -25,7 +59,7 @@ const login = () => {
<div class="field">
<label class="label">Password</label>
<p class="control has-icons-left">
<input id="ccn-login-form-password" class="input" type="password">
<input v-model="password" :disabled="isLoggingIn" class="input" type="password">
<span class="icon is-small is-left">
<font-awesome-icon icon="fas fa-lock"></font-awesome-icon>
</span>
@@ -33,7 +67,7 @@ const login = () => {
</div>
<div class="control">
<button class="button is-primary" @click="login">Login</button>
<button class="button is-primary" :disabled="isLoggingIn" @click="login">Login</button>
</div>
</div>