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 sys
import logging
from argparse import ArgumentParser from argparse import ArgumentParser
from typing import cast from typing import cast
from pathlib import Path from pathlib import Path
import server import server
import config import config
import utils import utils
import database import database
import logger
from logger import LOGGER, LoggerLevel
def GetUsernamePassword() -> tuple[str, str]: def GetUsernamePassword() -> tuple[str, str]:
@@ -26,15 +28,10 @@ def GetUsernamePassword() -> tuple[str, str]:
return (username, password) return (username, password)
def SetLoggingStyle(level: int) -> None:
logging.basicConfig(format="[%(levelname)s] %(message)s", level=level)
if __name__ == "__main__": if __name__ == "__main__":
# Set as INFO level in default first, # Set as INFO level in default first,
# and we will change it once we load the configuration file. # and we will change it once we load the configuration file.
SetLoggingStyle(logging.INFO) logger.set_level(LoggerLevel.INFO)
# Receive arguments # Receive arguments
parser = ArgumentParser( parser = ArgumentParser(
@@ -60,21 +57,21 @@ if __name__ == "__main__":
args = parser.parse_args() args = parser.parse_args()
# Show splash # Show splash
logging.info("Coconut-leaf") LOGGER.info("Coconut-leaf")
logging.info("A light, self-host and multi-account calendar system") LOGGER.info("A light, self-host and multi-account calendar system")
logging.info("Project: https://github.com/yyc12345/coconut-leaf") LOGGER.info("Project: https://github.com/yyc12345/coconut-leaf")
logging.info("===================") LOGGER.info("===================")
# Load config file # Load config file
try: try:
config.setup_config(cast(Path, args.config)) config.setup_config(cast(Path, args.config))
except Exception as e: except Exception as e:
logging.critical(f"Error loading config file: {e}") LOGGER.critical(f"Error loading config file: {e}")
sys.exit(1) sys.exit(1)
# Change logging level again according to whether enable debug mode # Change logging level again according to whether enable debug mode
logging_level = logging.DEBUG if config.get_config().others.debug else logging.INFO logging_level = LoggerLevel.DEBUG if config.get_config().others.debug else LoggerLevel.INFO
SetLoggingStyle(logging_level) logger.set_level(logging_level)
# Initialize the calendar system if needed # Initialize the calendar system if needed
if cast(bool, args.init): if cast(bool, args.init):
@@ -83,5 +80,5 @@ if __name__ == "__main__":
calendar.init(*gotten_data) calendar.init(*gotten_data)
calendar.close() calendar.close()
logging.info("Staring server...") LOGGER.info("Staring server...")
server.run() server.run()

View File

@@ -1,11 +1,23 @@
import config
import sqlite3 import sqlite3
import utils
import threading import threading
import logging
import dt
from typing import cast from typing import cast
from pathlib import Path 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 SafeDatabaseOperation(func):
def wrapper(self: 'CalendarDatabase', *args, **kwargs): def wrapper(self: 'CalendarDatabase', *args, **kwargs):
@@ -19,18 +31,18 @@ def SafeDatabaseOperation(func):
except Exception as e: except Exception as e:
self.cursor = None self.cursor = None
if cfg.others.debug: if cfg.others.debug:
logging.exception(e) LOGGER.exception(e)
return (False, str(e), None) return ResponseBody(False, str(e), None)
# do real data work # do real data work
try: try:
currentTime = utils.GetCurrentTimestamp() currentTime = utils.GetCurrentTimestamp()
if currentTime - self.latestClean > cfg.others.auto_token_clean_duration: if currentTime - self.latestClean > cfg.others.auto_token_clean_duration:
self.latestClean = currentTime self.latestClean = currentTime
logging.info('Cleaning outdated token...') LOGGER.info('Cleaning outdated token...')
self.tokenOper_clean() self.tokenOper_clean()
result = (True, '', func(self, *args, **kwargs)) result = ResponseBody(True, '', func(self, *args, **kwargs))
self.cursor.close() self.cursor.close()
self.cursor = None self.cursor = None
self.db.commit() self.db.commit()
@@ -40,8 +52,8 @@ def SafeDatabaseOperation(func):
self.cursor = None self.cursor = None
self.db.rollback() self.db.rollback()
if cfg.others.debug: if cfg.others.debug:
logging.exception(e) LOGGER.exception(e)
return (False, str(e), None) return ResponseBody(False, str(e), None)
return wrapper return wrapper
@@ -182,7 +194,12 @@ class CalendarDatabase:
@SafeDatabaseOperation @SafeDatabaseOperation
def common_webLogin(self, username, password, clientUa, clientIp): 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: if len(self.cursor.fetchall()) != 0:
token = utils.GenerateToken(username) token = utils.GenerateToken(username)
@@ -553,8 +570,8 @@ class CalendarDatabase:
argumentsList.append(_username) argumentsList.append(_username)
self.cursor.execute('UPDATE user SET {} WHERE [name] = ?;'.format(', '.join(sqlList)), self.cursor.execute('UPDATE user SET {} WHERE [name] = ?;'.format(', '.join(sqlList)),
tuple(argumentsList)) tuple(argumentsList))
logging.debug(cache) LOGGER.debug(cache)
logging.debug(tuple(argumentsList)) LOGGER.debug(tuple(argumentsList))
if self.cursor.rowcount != 1: if self.cursor.rowcount != 1:
raise Exception('Fail to update due to no matched rows or too much rows.') raise Exception('Fail to update due to no matched rows or too much rows.')
return True 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 Flask
from flask import request from flask import request
from dataclasses import dataclass
from typing import Any, Callable
import config import config
import database import database
import utils import utils
from logger import LOGGER
app = Flask(__name__) app = Flask(__name__)
calendar_db = database.CalendarDatabase() calendar_db = database.CalendarDatabase()
@@ -14,7 +18,7 @@ calendar_db = database.CalendarDatabase()
@app.route('/common/salt', methods=['POST']) @app.route('/common/salt', methods=['POST'])
def api_common_saltHandle(): def api_common_saltHandle():
return SmartDbCaller(calendar_db.common_salt, return SmartDbCaller(calendar_db.common_salt,
(('username', str, False), ), (FormField('username', str, False), ),
None) None)
@app.route('/common/login', methods=['POST']) @app.route('/common/login', methods=['POST'])
@@ -27,10 +31,10 @@ def api_common_loginHandle():
clientIp = request.remote_addr clientIp = request.remote_addr
return SmartDbCaller(calendar_db.common_login, return SmartDbCaller(calendar_db.common_login,
(('username', str, False), (FormField('username', str, False),
('password', str, False), FormField('password', str, False),
('clientUa', str, False), FormField('clientUa', str, False),
('clientIp', str, False)), FormField('clientIp', str, False)),
{ {
'clientUa': clientUa, 'clientUa': clientUa,
'clientIp': clientIp 'clientIp': clientIp
@@ -44,12 +48,12 @@ def api_common_webLoginHandle():
clientIp = request.headers.getlist("X-Forwarded-For")[0] clientIp = request.headers.getlist("X-Forwarded-For")[0]
else: else:
clientIp = request.remote_addr clientIp = request.remote_addr
return SmartDbCaller(calendar_db.common_webLogin, return SmartDbCaller(calendar_db.common_webLogin,
(('username', str, False), (FormField('username', str, False),
('password', str, False), FormField('password', str, False),
('clientUa', str, False), FormField('clientUa', str, False),
('clientIp', str, False)), FormField('clientIp', str, False)),
{ {
'clientUa': clientUa, 'clientUa': clientUa,
'clientIp': clientIp 'clientIp': clientIp
@@ -58,13 +62,13 @@ def api_common_webLoginHandle():
@app.route('/common/logout', methods=['POST']) @app.route('/common/logout', methods=['POST'])
def api_common_logoutHandle(): def api_common_logoutHandle():
return SmartDbCaller(calendar_db.common_logout, return SmartDbCaller(calendar_db.common_logout,
(('token', str, False), ), (FormField('token', str, False), ),
None) None)
@app.route('/common/tokenValid', methods=['POST']) @app.route('/common/tokenValid', methods=['POST'])
def api_common_tokenValidHandle(): def api_common_tokenValidHandle():
return SmartDbCaller(calendar_db.common_tokenValid, return SmartDbCaller(calendar_db.common_tokenValid,
(('token', str, False), ), (FormField('token', str, False), ),
None) None)
# endregion # endregion
@@ -74,60 +78,60 @@ def api_common_tokenValidHandle():
@app.route('/calendar/getFull', methods=['POST']) @app.route('/calendar/getFull', methods=['POST'])
def api_calendar_getFullHandle(): def api_calendar_getFullHandle():
return SmartDbCaller(calendar_db.calendar_getFull, return SmartDbCaller(calendar_db.calendar_getFull,
(('token', str, False), (FormField('token', str, False),
('startDateTime', int, False), FormField('startDateTime', int, False),
('endDateTime', int, False)), FormField('endDateTime', int, False)),
None) None)
@app.route('/calendar/getList', methods=['POST']) @app.route('/calendar/getList', methods=['POST'])
def api_calendar_getListHandle(): def api_calendar_getListHandle():
return SmartDbCaller(calendar_db.calendar_getList, return SmartDbCaller(calendar_db.calendar_getList,
(('token', str, False), (FormField('token', str, False),
('startDateTime', int, False), FormField('startDateTime', int, False),
('endDateTime', int, False)), FormField('endDateTime', int, False)),
None) None)
@app.route('/calendar/getDetail', methods=['POST']) @app.route('/calendar/getDetail', methods=['POST'])
def api_calendar_getDetailHandle(): def api_calendar_getDetailHandle():
return SmartDbCaller(calendar_db.calendar_getDetail, return SmartDbCaller(calendar_db.calendar_getDetail,
(('token', str, False), (FormField('token', str, False),
('uuid', str, False)), FormField('uuid', str, False)),
None) None)
@app.route('/calendar/update', methods=['POST']) @app.route('/calendar/update', methods=['POST'])
def api_calendar_updateHandle(): def api_calendar_updateHandle():
return SmartDbCaller(calendar_db.calendar_update, return SmartDbCaller(calendar_db.calendar_update,
(('token', str, False), (FormField('token', str, False),
('uuid', str, False), FormField('uuid', str, False),
('belongTo', str, True), FormField('belongTo', str, True),
('title', str, True), FormField('title', str, True),
('description', str, True), FormField('description', str, True),
('eventDateTimeStart', int, True), FormField('eventDateTimeStart', int, True),
('eventDateTimeEnd', int, True), FormField('eventDateTimeEnd', int, True),
('loopRules', str, True), FormField('loopRules', str, True),
('timezoneOffset', int, True), FormField('timezoneOffset', int, True),
('lastChange', str, False)), FormField('lastChange', str, False)),
None) None)
@app.route('/calendar/add', methods=['POST']) @app.route('/calendar/add', methods=['POST'])
def api_calendar_addHandle(): def api_calendar_addHandle():
return SmartDbCaller(calendar_db.calendar_add, return SmartDbCaller(calendar_db.calendar_add,
(('token', str, False), (FormField('token', str, False),
('belongTo', str, False), FormField('belongTo', str, False),
('title', str, False), FormField('title', str, False),
('description', str, False), FormField('description', str, False),
('eventDateTimeStart', int, False), FormField('eventDateTimeStart', int, False),
('eventDateTimeEnd', int, False), FormField('eventDateTimeEnd', int, False),
('loopRules', str, False), FormField('loopRules', str, False),
('timezoneOffset', int, False)), FormField('timezoneOffset', int, False)),
None) None)
@app.route('/calendar/delete', methods=['POST']) @app.route('/calendar/delete', methods=['POST'])
def api_calendar_deleteHandle(): def api_calendar_deleteHandle():
return SmartDbCaller(calendar_db.calendar_delete, return SmartDbCaller(calendar_db.calendar_delete,
(('token', str, False), (FormField('token', str, False),
('uuid', str, False), FormField('uuid', str, False),
('lastChange', str, False)), FormField('lastChange', str, False)),
None) None)
# endregion # endregion
@@ -137,77 +141,77 @@ def api_calendar_deleteHandle():
@app.route('/collection/getFullOwn', methods=['POST']) @app.route('/collection/getFullOwn', methods=['POST'])
def api_collection_getFullOwnHandle(): def api_collection_getFullOwnHandle():
return SmartDbCaller(calendar_db.collection_getFullOwn, return SmartDbCaller(calendar_db.collection_getFullOwn,
(('token', str, False), ), (FormField('token', str, False), ),
None) None)
@app.route('/collection/getListOwn', methods=['POST']) @app.route('/collection/getListOwn', methods=['POST'])
def api_collection_getListOwnHandle(): def api_collection_getListOwnHandle():
return SmartDbCaller(calendar_db.collection_getListOwn, return SmartDbCaller(calendar_db.collection_getListOwn,
(('token', str, False), ), (FormField('token', str, False), ),
None) None)
@app.route('/collection/getDetailOwn', methods=['POST']) @app.route('/collection/getDetailOwn', methods=['POST'])
def api_collection_getDetailOwnHandle(): def api_collection_getDetailOwnHandle():
return SmartDbCaller(calendar_db.collection_getDetailOwn, return SmartDbCaller(calendar_db.collection_getDetailOwn,
(('token', str, False), (FormField('token', str, False),
('uuid', str, False)), FormField('uuid', str, False)),
None) None)
@app.route('/collection/addOwn', methods=['POST']) @app.route('/collection/addOwn', methods=['POST'])
def api_collection_addOwnHandle(): def api_collection_addOwnHandle():
return SmartDbCaller(calendar_db.collection_addOwn, return SmartDbCaller(calendar_db.collection_addOwn,
(('token', str, False), (FormField('token', str, False),
('name', str, False)), FormField('name', str, False)),
None) None)
@app.route('/collection/updateOwn', methods=['POST']) @app.route('/collection/updateOwn', methods=['POST'])
def api_collection_updateOwnHandle(): def api_collection_updateOwnHandle():
return SmartDbCaller(calendar_db.collection_updateOwn, return SmartDbCaller(calendar_db.collection_updateOwn,
(('token', str, False), (FormField('token', str, False),
('uuid', str, False), FormField('uuid', str, False),
('name', str, False), FormField('name', str, False),
('lastChange', str, False)), FormField('lastChange', str, False)),
None) None)
@app.route('/collection/deleteOwn', methods=['POST']) @app.route('/collection/deleteOwn', methods=['POST'])
def api_collection_deleteOwnHandle(): def api_collection_deleteOwnHandle():
return SmartDbCaller(calendar_db.collection_deleteOwn, return SmartDbCaller(calendar_db.collection_deleteOwn,
(('token', str, False), (FormField('token', str, False),
('uuid', str, False), FormField('uuid', str, False),
('lastChange', str, False)), FormField('lastChange', str, False)),
None) None)
@app.route('/collection/getSharing', methods=['POST']) @app.route('/collection/getSharing', methods=['POST'])
def api_collection_getSharingHandle(): def api_collection_getSharingHandle():
return SmartDbCaller(calendar_db.collection_getSharing, return SmartDbCaller(calendar_db.collection_getSharing,
(('token', str, False), (FormField('token', str, False),
('uuid', str, False)), FormField('uuid', str, False)),
None) None)
@app.route('/collection/deleteSharing', methods=['POST']) @app.route('/collection/deleteSharing', methods=['POST'])
def api_collection_deleteSharingHandle(): def api_collection_deleteSharingHandle():
return SmartDbCaller(calendar_db.collection_deleteSharing, return SmartDbCaller(calendar_db.collection_deleteSharing,
(('token', str, False), (FormField('token', str, False),
('uuid', str, False), FormField('uuid', str, False),
('target', str, False), FormField('target', str, False),
('lastChange', str, False)), FormField('lastChange', str, False)),
None) None)
@app.route('/collection/addSharing', methods=['POST']) @app.route('/collection/addSharing', methods=['POST'])
def api_collection_addSharingHandle(): def api_collection_addSharingHandle():
return SmartDbCaller(calendar_db.collection_addSharing, return SmartDbCaller(calendar_db.collection_addSharing,
(('token', str, False), (FormField('token', str, False),
('uuid', str, False), FormField('uuid', str, False),
('target', str, False), FormField('target', str, False),
('lastChange', str, False)), FormField('lastChange', str, False)),
None) None)
@app.route('/collection/getShared', methods=['POST']) @app.route('/collection/getShared', methods=['POST'])
def api_collection_getSharedHandle(): def api_collection_getSharedHandle():
return SmartDbCaller(calendar_db.collection_getShared, return SmartDbCaller(calendar_db.collection_getShared,
(('token', str, False), ), (FormField('token', str, False), ),
None) None)
# endregion # endregion
@@ -217,43 +221,43 @@ def api_collection_getSharedHandle():
@app.route('/todo/getFull', methods=['POST']) @app.route('/todo/getFull', methods=['POST'])
def api_todo_getFullHandle(): def api_todo_getFullHandle():
return SmartDbCaller(calendar_db.todo_getFull, return SmartDbCaller(calendar_db.todo_getFull,
(('token', str, False), ), (FormField('token', str, False), ),
None) None)
@app.route('/todo/getList', methods=['POST']) @app.route('/todo/getList', methods=['POST'])
def api_todo_getListHandle(): def api_todo_getListHandle():
return SmartDbCaller(calendar_db.todo_getList, return SmartDbCaller(calendar_db.todo_getList,
(('token', str, False), ), (FormField('token', str, False), ),
None) None)
@app.route('/todo/getDetail', methods=['POST']) @app.route('/todo/getDetail', methods=['POST'])
def api_todo_getDetailHandle(): def api_todo_getDetailHandle():
return SmartDbCaller(calendar_db.todo_getDetail, return SmartDbCaller(calendar_db.todo_getDetail,
(('token', str, False), (FormField('token', str, False),
('uuid', str, False)), FormField('uuid', str, False)),
None) None)
@app.route('/todo/add', methods=['POST']) @app.route('/todo/add', methods=['POST'])
def api_todo_addHandle(): def api_todo_addHandle():
return SmartDbCaller(calendar_db.todo_add, return SmartDbCaller(calendar_db.todo_add,
(('token', str, False), ), (FormField('token', str, False), ),
None) None)
@app.route('/todo/update', methods=['POST']) @app.route('/todo/update', methods=['POST'])
def api_todo_updateHandle(): def api_todo_updateHandle():
return SmartDbCaller(calendar_db.todo_update, return SmartDbCaller(calendar_db.todo_update,
(('token', str, False), (FormField('token', str, False),
('uuid', str, False), FormField('uuid', str, False),
('data', str, False), FormField('data', str, False),
('lastChange', str, False)), FormField('lastChange', str, False)),
None) None)
@app.route('/todo/delete', methods=['POST']) @app.route('/todo/delete', methods=['POST'])
def api_todo_deleteHandle(): def api_todo_deleteHandle():
return SmartDbCaller(calendar_db.todo_delete, return SmartDbCaller(calendar_db.todo_delete,
(('token', str, False), (FormField('token', str, False),
('uuid', str, False), FormField('uuid', str, False),
('lastChange', str, False)), FormField('lastChange', str, False)),
None) None)
# endregion # endregion
@@ -263,30 +267,30 @@ def api_todo_deleteHandle():
@app.route('/admin/get', methods=['POST']) @app.route('/admin/get', methods=['POST'])
def api_admin_getHandle(): def api_admin_getHandle():
return SmartDbCaller(calendar_db.admin_get, return SmartDbCaller(calendar_db.admin_get,
(('token', str, False), ), (FormField('token', str, False), ),
None) None)
@app.route('/admin/add', methods=['POST']) @app.route('/admin/add', methods=['POST'])
def api_admin_addHandle(): def api_admin_addHandle():
return SmartDbCaller(calendar_db.admin_add, return SmartDbCaller(calendar_db.admin_add,
(('token', str, False), (FormField('token', str, False),
('username', str, False)), FormField('username', str, False)),
None) None)
@app.route('/admin/update', methods=['POST']) @app.route('/admin/update', methods=['POST'])
def api_admin_updateHandle(): def api_admin_updateHandle():
return SmartDbCaller(calendar_db.admin_update, return SmartDbCaller(calendar_db.admin_update,
(('token', str, False), (FormField('token', str, False),
('username', str, False), FormField('username', str, False),
('password', str, True), FormField('password', str, True),
('isAdmin', utils.Str2Bool, True)), FormField('isAdmin', utils.Str2Bool, True)),
None) None)
@app.route('/admin/delete', methods=['POST']) @app.route('/admin/delete', methods=['POST'])
def api_admin_deleteHandle(): def api_admin_deleteHandle():
return SmartDbCaller(calendar_db.admin_delete, return SmartDbCaller(calendar_db.admin_delete,
(('token', str, False), (FormField('token', str, False),
('username', str, False)), FormField('username', str, False)),
None) None)
# endregion # endregion
@@ -296,27 +300,27 @@ def api_admin_deleteHandle():
@app.route('/profile/isAdmin', methods=['POST']) @app.route('/profile/isAdmin', methods=['POST'])
def api_profile_isAdminHandle(): def api_profile_isAdminHandle():
return SmartDbCaller(calendar_db.profile_isAdmin, return SmartDbCaller(calendar_db.profile_isAdmin,
(('token', str, False), ), (FormField('token', str, False), ),
None) None)
@app.route('/profile/changePassword', methods=['POST']) @app.route('/profile/changePassword', methods=['POST'])
def api_profile_changePasswordHandle(): def api_profile_changePasswordHandle():
return SmartDbCaller(calendar_db.profile_changePassword, return SmartDbCaller(calendar_db.profile_changePassword,
(('token', str, False), (FormField('token', str, False),
('password', str, False)), FormField('password', str, False)),
None) None)
@app.route('/profile/getToken', methods=['POST']) @app.route('/profile/getToken', methods=['POST'])
def api_profile_getTokenHandle(): def api_profile_getTokenHandle():
return SmartDbCaller(calendar_db.profile_getToken, return SmartDbCaller(calendar_db.profile_getToken,
(('token', str, False), ), (FormField('token', str, False), ),
None) None)
@app.route('/profile/deleteToken', methods=['POST']) @app.route('/profile/deleteToken', methods=['POST'])
def api_profile_deleteTokenHandle(): def api_profile_deleteTokenHandle():
return SmartDbCaller(calendar_db.profile_deleteToken, return SmartDbCaller(calendar_db.profile_deleteToken,
(('token', str, False), (FormField('token', str, False),
('deleteToken', str, False)), FormField('deleteToken', str, False)),
None) None)
# endregion # endregion
@@ -325,41 +329,64 @@ def api_profile_deleteTokenHandle():
# region: Misc Functions # region: Misc Functions
def SmartDbCaller(dbMethod, paramTuple, extraDict): @dataclass(frozen=True)
result = (False, 'Invalid parameter', None) class FormField:
optCount = 0 name: str
paramList = [] """The name of form field."""
optParamDict = {} ty: Callable[[str], Any]
# for each item, """The type of form field."""
# item[0] is field name. is_optional: bool
# item[1] is type. """True if this form field is optional, otherwise false."""
# item[2] is whether it is optional field
realForm = request.form.to_dict() def SmartDbCaller(db_method: Callable, fields: tuple[FormField, ...], padding_form: dict[str, Any] | None) -> dict[str, Any]:
if extraDict is not None: result = ResponseBody(False, 'Invalid parameter', None)
realForm.update(extraDict) opt_param_counter = 0
for item in paramTuple: param_list: list[Any] = []
cache = item[1](realForm.get(item[0], None)) opt_param_dict: dict[str, Any] = {}
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)
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) 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 { return {
'success': returnedTuple[0], 'success': body.success,
'error': returnedTuple[1], 'error': body.error,
'data': returnedTuple[2] 'data': body.data
} }
def run(): def run():

View File

@@ -17,6 +17,7 @@
"@fortawesome/fontawesome-svg-core": "^7.2.0", "@fortawesome/fontawesome-svg-core": "^7.2.0",
"@fortawesome/free-solid-svg-icons": "^7.2.0", "@fortawesome/free-solid-svg-icons": "^7.2.0",
"@fortawesome/vue-fontawesome": "^3.2.0", "@fortawesome/vue-fontawesome": "^3.2.0",
"axios": "1.14.0",
"bulma": "0.9.1", "bulma": "0.9.1",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1", "pinia-plugin-persistedstate": "^4.7.1",

191
frontend/pnpm-lock.yaml generated
View File

@@ -17,6 +17,9 @@ importers:
'@fortawesome/vue-fontawesome': '@fortawesome/vue-fontawesome':
specifier: ^3.2.0 specifier: ^3.2.0
version: 3.2.0(@fortawesome/fontawesome-svg-core@7.2.0)(vue@3.5.33(typescript@6.0.3)) 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: bulma:
specifier: 0.9.1 specifier: 0.9.1
version: 0.9.1 version: 0.9.1
@@ -876,6 +879,12 @@ packages:
resolution: {integrity: sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==} resolution: {integrity: sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==}
engines: {node: '>=20.19.0'} 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: balanced-match@4.0.4:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22} engines: {node: 18 || 20 || >=22}
@@ -911,6 +920,10 @@ packages:
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
engines: {node: '>=18'} 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: caniuse-lite@1.0.30001791:
resolution: {integrity: sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==} resolution: {integrity: sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==}
@@ -922,6 +935,10 @@ packages:
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
engines: {node: '>= 20.19.0'} engines: {node: '>= 20.19.0'}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
confbox@0.1.8: confbox@0.1.8:
resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
@@ -974,10 +991,18 @@ packages:
defu@6.1.7: defu@6.1.7:
resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} 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: detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
electron-to-chromium@1.5.344: electron-to-chromium@1.5.344:
resolution: {integrity: sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==} resolution: {integrity: sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==}
@@ -988,6 +1013,22 @@ packages:
error-stack-parser-es@1.0.5: error-stack-parser-es@1.0.5:
resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} 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: escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -1107,15 +1148,39 @@ packages:
flatted@3.4.2: flatted@3.4.2:
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} 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: fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin] os: [darwin]
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
gensync@1.0.0-beta.2: gensync@1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'} 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: glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@@ -1124,6 +1189,22 @@ packages:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'} 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: hookable@5.5.3:
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
@@ -1314,6 +1395,10 @@ packages:
magic-string@0.30.21: magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} 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: memorystream@0.3.1:
resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
engines: {node: '>= 0.10.0'} engines: {node: '>= 0.10.0'}
@@ -1326,6 +1411,14 @@ packages:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'} 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: minimatch@10.2.5:
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
engines: {node: 18 || 20 || >=22} engines: {node: 18 || 20 || >=22}
@@ -1478,6 +1571,10 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'} 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: punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -2572,6 +2669,16 @@ snapshots:
'@babel/parser': 7.29.2 '@babel/parser': 7.29.2
ast-kit: 2.2.0 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: {} balanced-match@4.0.4: {}
baseline-browser-mapping@2.10.23: {} baseline-browser-mapping@2.10.23: {}
@@ -2602,6 +2709,11 @@ snapshots:
dependencies: dependencies:
run-applescript: 7.1.0 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: {} caniuse-lite@1.0.30001791: {}
chokidar@4.0.3: chokidar@4.0.3:
@@ -2613,6 +2725,10 @@ snapshots:
dependencies: dependencies:
readdirp: 5.0.0 readdirp: 5.0.0
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
confbox@0.1.8: {} confbox@0.1.8: {}
confbox@0.2.4: {} confbox@0.2.4: {}
@@ -2650,14 +2766,37 @@ snapshots:
defu@6.1.7: {} defu@6.1.7: {}
delayed-stream@1.0.0: {}
detect-libc@2.1.2: {} 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: {} electron-to-chromium@1.5.344: {}
entities@7.0.1: {} entities@7.0.1: {}
error-stack-parser-es@1.0.5: {} 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: {} escalade@3.2.0: {}
escape-string-regexp@4.0.0: {} escape-string-regexp@4.0.0: {}
@@ -2792,11 +2931,41 @@ snapshots:
flatted@3.4.2: {} 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: fsevents@2.3.3:
optional: true optional: true
function-bind@1.1.2: {}
gensync@1.0.0-beta.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: glob-parent@5.1.2:
dependencies: dependencies:
is-glob: 4.0.3 is-glob: 4.0.3
@@ -2805,6 +2974,18 @@ snapshots:
dependencies: dependencies:
is-glob: 4.0.3 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: {} hookable@5.5.3: {}
ignore@5.3.2: {} ignore@5.3.2: {}
@@ -2940,6 +3121,8 @@ snapshots:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
math-intrinsics@1.1.0: {}
memorystream@0.3.1: {} memorystream@0.3.1: {}
merge2@1.4.1: {} merge2@1.4.1: {}
@@ -2949,6 +3132,12 @@ snapshots:
braces: 3.0.3 braces: 3.0.3
picomatch: 2.3.2 picomatch: 2.3.2
mime-db@1.52.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
minimatch@10.2.5: minimatch@10.2.5:
dependencies: dependencies:
brace-expansion: 5.0.5 brace-expansion: 5.0.5
@@ -3100,6 +3289,8 @@ snapshots:
prelude-ls@1.2.1: {} prelude-ls@1.2.1: {}
proxy-from-env@2.1.0: {}
punycode@2.3.1: {} punycode@2.3.1: {}
quansync@0.2.11: {} 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 export default router

View File

@@ -1,11 +1,45 @@
<script setup lang="ts"> <script setup lang="ts">
import MessageBox from '@/components/MessageBox.vue';
import { ref } from '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 messagebox = ref<InstanceType<typeof MessageBox> | null>(null);
const login = () => { const login = async () => {
messagebox.value?.show("Fail to login. Please check your username or password.") // 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> </script>
@@ -16,7 +50,7 @@ const login = () => {
<div class="field"> <div class="field">
<label class="label">User Name</label> <label class="label">User Name</label>
<div class="control has-icons-left has-icons-right"> <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"> <span class="icon is-small is-left">
<font-awesome-icon icon="fas fa-user"></font-awesome-icon> <font-awesome-icon icon="fas fa-user"></font-awesome-icon>
</span> </span>
@@ -25,7 +59,7 @@ const login = () => {
<div class="field"> <div class="field">
<label class="label">Password</label> <label class="label">Password</label>
<p class="control has-icons-left"> <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"> <span class="icon is-small is-left">
<font-awesome-icon icon="fas fa-lock"></font-awesome-icon> <font-awesome-icon icon="fas fa-lock"></font-awesome-icon>
</span> </span>
@@ -33,7 +67,7 @@ const login = () => {
</div> </div>
<div class="control"> <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>
</div> </div>