Compare commits
11 Commits
46f2d69800
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| c4b68400b3 | |||
| d7df194a12 | |||
| 35fee0f473 | |||
| e484ded5be | |||
| 2a280dcba0 | |||
| 6337ae432d | |||
| 826cbf18b1 | |||
| 167c83f7d4 | |||
| 078e61e993 | |||
| bdee3b3efa | |||
| 37b08927a7 |
101
assets/ics2csv.py
Normal file
101
assets/ics2csv.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import icalendar
|
||||||
|
|
||||||
|
def DumpComponentHeader(file, component):
|
||||||
|
fields = []
|
||||||
|
inclusiveUnitCounter = 0
|
||||||
|
fields += map(lambda x: x + ' - required', component.required)
|
||||||
|
fields += map(lambda x: x + ' - singletons', component.singletons)
|
||||||
|
for units in component.inclusive:
|
||||||
|
fields += map(lambda x: x + ' - inclusive{}'.format(inclusiveUnitCounter), units)
|
||||||
|
inclusiveUnitCounter += 1
|
||||||
|
fields += ('exclusive - name', 'exclusive - data')
|
||||||
|
fields += ('multiple', )
|
||||||
|
file.write(','.join(fields))
|
||||||
|
file.write('\n')
|
||||||
|
|
||||||
|
def DumpComponentData(file, component):
|
||||||
|
data = []
|
||||||
|
gotten_instance = None
|
||||||
|
|
||||||
|
for item in component.required:
|
||||||
|
gotten_instance = component.get(item)
|
||||||
|
if gotten_instance is not None:
|
||||||
|
data.append(AdvancedFormater(gotten_instance))
|
||||||
|
else:
|
||||||
|
data.append('')
|
||||||
|
for item in component.singletons:
|
||||||
|
gotten_instance = component.get(item)
|
||||||
|
if gotten_instance is not None:
|
||||||
|
data.append(AdvancedFormater(gotten_instance))
|
||||||
|
else:
|
||||||
|
data.append('')
|
||||||
|
for units in component.inclusive:
|
||||||
|
for item in units:
|
||||||
|
gotten_instance = component.get(item)
|
||||||
|
if gotten_instance is not None:
|
||||||
|
data.append(AdvancedFormater(gotten_instance))
|
||||||
|
else:
|
||||||
|
data.append('')
|
||||||
|
|
||||||
|
gotten_name = ""
|
||||||
|
gotten_data = ""
|
||||||
|
for item in component.exclusive:
|
||||||
|
gotten_instance = component.get(item)
|
||||||
|
if gotten_instance is not None:
|
||||||
|
gotten_name = item
|
||||||
|
gotten_data = AdvancedFormater(gotten_instance)
|
||||||
|
break
|
||||||
|
data.append(gotten_name)
|
||||||
|
data.append(gotten_data)
|
||||||
|
|
||||||
|
for item in component.multiple:
|
||||||
|
gotten_instance = component.get(item)
|
||||||
|
if gotten_instance is not None:
|
||||||
|
data.append('- {} -'.format(item))
|
||||||
|
data.append(AdvancedFormater(gotten_instance))
|
||||||
|
else:
|
||||||
|
data.append('')
|
||||||
|
|
||||||
|
file.write(','.join(data))
|
||||||
|
file.write('\n')
|
||||||
|
|
||||||
|
def AdvancedFormater(data):
|
||||||
|
if isinstance(data, icalendar.prop.vDDDTypes):
|
||||||
|
return str(data.dt)
|
||||||
|
else:
|
||||||
|
return str(data)
|
||||||
|
|
||||||
|
# read file
|
||||||
|
icsFile = open('test.ics', 'rb')
|
||||||
|
cal = icalendar.Calendar.from_ical(icsFile.read())
|
||||||
|
icsFile.close()
|
||||||
|
|
||||||
|
# analyse file
|
||||||
|
csvEvent = open('event.csv', 'w')
|
||||||
|
csvEventHeader = False
|
||||||
|
csvAlarm = open('alarm.csv', 'w')
|
||||||
|
csvAlarmHeader = False
|
||||||
|
|
||||||
|
eventCount = 0
|
||||||
|
alarmCount = 0
|
||||||
|
miscCount = 0
|
||||||
|
for component in cal.walk():
|
||||||
|
if component.name == 'VEVENT':
|
||||||
|
eventCount += 1
|
||||||
|
if not csvEventHeader:
|
||||||
|
DumpComponentHeader(csvEvent, component)
|
||||||
|
csvEventHeader = True
|
||||||
|
DumpComponentData(csvEvent, component)
|
||||||
|
elif component.name == 'VALARM':
|
||||||
|
alarmCount += 1
|
||||||
|
if not csvAlarmHeader:
|
||||||
|
DumpComponentHeader(csvAlarm, component)
|
||||||
|
csvAlarmHeader = True
|
||||||
|
DumpComponentData(csvAlarm, component)
|
||||||
|
else:
|
||||||
|
miscCount += 1
|
||||||
|
|
||||||
|
|
||||||
|
csvEvent.close()
|
||||||
|
csvAlarm.close()
|
||||||
|
print('Event count: {}\nAlarm count: {}\nMisc count: {}'.format(eventCount, alarmCount, miscCount))
|
||||||
156
assets/ics_converter.py
Normal file
156
assets/ics_converter.py
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import icalendar
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import database
|
||||||
|
import json
|
||||||
|
import datetime
|
||||||
|
import dt as localdt
|
||||||
|
|
||||||
|
def AdvancedDatetTimeGet(dt, isStartDateTime):
|
||||||
|
if isinstance(dt, datetime.datetime):
|
||||||
|
gottenDatetime = int(dt.timestamp() / 60)
|
||||||
|
elif isinstance(dt, datetime.date):
|
||||||
|
gottenDatetime = int(datetime.datetime(
|
||||||
|
dt.year,
|
||||||
|
dt.month,
|
||||||
|
dt.day,
|
||||||
|
0 if isStartDateTime else 23,
|
||||||
|
0 if isStartDateTime else 59,
|
||||||
|
0 if isStartDateTime else 59,
|
||||||
|
0, tzinfo=LOCAL_TZ
|
||||||
|
).timestamp() / 60)
|
||||||
|
else:
|
||||||
|
raise Exception('Unexpected data')
|
||||||
|
|
||||||
|
timezoneOffset = LOCAL_UTC_OFFSET
|
||||||
|
return (gottenDatetime, timezoneOffset)
|
||||||
|
|
||||||
|
def AdvancedDateTimeAnalyser(component):
|
||||||
|
startDatetimeRef = component.get('DTSTART').dt
|
||||||
|
(startDatetime, timezoneOffset) = AdvancedDatetTimeGet(startDatetimeRef, True)
|
||||||
|
|
||||||
|
if component.get('DTEND') is not None:
|
||||||
|
(endDatetime, _) = AdvancedDatetTimeGet(startDatetimeRef, False)
|
||||||
|
elif component.get('DURATION') is not None:
|
||||||
|
endDurationRef = component.get('DURATION').dt
|
||||||
|
if isinstance(endDurationRef, datetime.timedelta):
|
||||||
|
endDatetime = startDatetime + int(endDurationRef.total_seconds() / 60)
|
||||||
|
else:
|
||||||
|
raise Exception('Unexpected data')
|
||||||
|
else:
|
||||||
|
raise Exception('Unexpected data')
|
||||||
|
|
||||||
|
return (startDatetime, endDatetime, timezoneOffset)
|
||||||
|
|
||||||
|
def LoopRulesConverter(component):
|
||||||
|
jsonData = component.get('RRULE')
|
||||||
|
if jsonData is None:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
loopRules = ""
|
||||||
|
loopStopRules = ""
|
||||||
|
freq = jsonData.get('FREQ')[0]
|
||||||
|
if freq == 'MONTHLY':
|
||||||
|
loopRules = 'MSA{}'.format(str(jsonData.get('INTERVAL')[0]))
|
||||||
|
elif freq == 'WEEKLY':
|
||||||
|
occupiedWeek = [False, ] * 7
|
||||||
|
for item in jsonData.get('BYDAY'):
|
||||||
|
occupiedWeek[WEEK_DICT[item]] = True
|
||||||
|
loopRules = 'W{}{}'.format(
|
||||||
|
''.join(map(lambda x: 'T' if x else 'F', occupiedWeek)),
|
||||||
|
str(jsonData.get('INTERVAL')[0])
|
||||||
|
)
|
||||||
|
elif freq == 'YEARLY':
|
||||||
|
loopRules = 'YS{}'.format(str(jsonData.get('INTERVAL')[0]))
|
||||||
|
else:
|
||||||
|
raise Exception('Unexpected data')
|
||||||
|
|
||||||
|
if jsonData.get('COUNT') is not None:
|
||||||
|
loopStopRules = 'T{}'.format(str(jsonData.get('COUNT')[0]))
|
||||||
|
else:
|
||||||
|
loopStopRules = 'F'
|
||||||
|
|
||||||
|
return loopRules + '-' + loopStopRules
|
||||||
|
|
||||||
|
# ============================ read args
|
||||||
|
icsFilePath = sys.argv[1]
|
||||||
|
if not os.path.isfile(icsFilePath):
|
||||||
|
print('Fail to load ics file')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# read file
|
||||||
|
icsFile = open(icsFilePath, 'rb')
|
||||||
|
cal = icalendar.Calendar.from_ical(icsFile.read())
|
||||||
|
icsFile.close()
|
||||||
|
|
||||||
|
# ============================ init const
|
||||||
|
utfOffset = float(input('Input this ics file\'s utc offset (time unit: hour)>'))
|
||||||
|
LOCAL_UTC_OFFSET = int(utfOffset * 60)
|
||||||
|
LOCAL_TZ = localdt.UTCTimezone(LOCAL_UTC_OFFSET)
|
||||||
|
WEEK_DICT = {
|
||||||
|
"SU": 6, "MO": 0, "TU": 1, "WE": 2, "TH": 3, "FR": 4, "SA": 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================ pick database
|
||||||
|
|
||||||
|
db = database.CalendarDatabase()
|
||||||
|
db.open()
|
||||||
|
username = input('Input username >')
|
||||||
|
password = input('Input password >')
|
||||||
|
(status, error, token) = db.common_webLogin(username, password, 'Python backend', '127.0.0.1')
|
||||||
|
if not status:
|
||||||
|
print('Fail to login.')
|
||||||
|
sys.exit(1)
|
||||||
|
(status, error, collectionList) = db.collection_getFullOwn(token)
|
||||||
|
if not status:
|
||||||
|
print('Database return an error')
|
||||||
|
sys.exit(1)
|
||||||
|
print('Pick a collection to insert imported events')
|
||||||
|
counter = 0
|
||||||
|
for i in collectionList:
|
||||||
|
print('{}\t{}'.format(counter, i[1]))
|
||||||
|
counter += 1
|
||||||
|
pickedIndex = int(input())
|
||||||
|
collectionUuid = collectionList[pickedIndex][0]
|
||||||
|
|
||||||
|
# ============================ analyse file
|
||||||
|
eventCount = 0
|
||||||
|
allCount = 0
|
||||||
|
for component in cal.walk():
|
||||||
|
allCount += 1
|
||||||
|
# only import event chunk
|
||||||
|
if component.name == 'VEVENT':
|
||||||
|
eventCount += 1
|
||||||
|
title = str(component.get('SUMMARY'))
|
||||||
|
descriptionPrototype = {
|
||||||
|
'color': '#1e90ff',
|
||||||
|
'description': None
|
||||||
|
}
|
||||||
|
descriptionList = []
|
||||||
|
if component.get('DESCRIPTION') is not None and str(component.get('DESCRIPTION')) != '':
|
||||||
|
descriptionList.append(component.get('DESCRIPTION'))
|
||||||
|
if component.get('LOCATION') is not None and str(component.get('LOCATION')) != '':
|
||||||
|
descriptionList.append(component.get('LOCATION'))
|
||||||
|
descriptionPrototype['description'] = '\n'.join(descriptionList)
|
||||||
|
description = json.dumps(descriptionPrototype)
|
||||||
|
|
||||||
|
(eventDateTimeStart, eventDateTimeEnd, timezoneOffset) = AdvancedDateTimeAnalyser(component)
|
||||||
|
loopRules = LoopRulesConverter(component)
|
||||||
|
|
||||||
|
(status, _, _) = db.calendar_add(
|
||||||
|
token,
|
||||||
|
collectionUuid,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
eventDateTimeStart,
|
||||||
|
eventDateTimeEnd,
|
||||||
|
loopRules,
|
||||||
|
timezoneOffset
|
||||||
|
)
|
||||||
|
if not status:
|
||||||
|
print('Database return an error')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
db.common_logout(token)
|
||||||
|
db.close()
|
||||||
|
print('All chunk: {}\nEvent count: {}'.format(allCount, eventCount))
|
||||||
3
assets/migration/README.md
Normal file
3
assets/migration/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Migration
|
||||||
|
|
||||||
|
This directory contains the migration scripts for the database.
|
||||||
49
assets/migration/v1_to_v2.sqlite.sql
Normal file
49
assets/migration/v1_to_v2.sqlite.sql
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
-- Migration script for coconut-leaf database v1 to v2
|
||||||
|
-- This script updates field names by:
|
||||||
|
-- 1. Removing 'ccn_' prefix from all fields
|
||||||
|
-- 2. Converting camelCase to snake_case
|
||||||
|
|
||||||
|
-- Step 1: Rename user table columns
|
||||||
|
ALTER TABLE user RENAME COLUMN ccn_name TO name;
|
||||||
|
ALTER TABLE user RENAME COLUMN ccn_password TO password;
|
||||||
|
ALTER TABLE user RENAME COLUMN ccn_isAdmin TO is_admin;
|
||||||
|
ALTER TABLE user RENAME COLUMN ccn_salt TO salt;
|
||||||
|
|
||||||
|
-- Step 2: Rename token table columns
|
||||||
|
ALTER TABLE token RENAME COLUMN ccn_user TO user;
|
||||||
|
ALTER TABLE token RENAME COLUMN ccn_token TO token;
|
||||||
|
ALTER TABLE token RENAME COLUMN ccn_tokenExpireOn TO token_expire_on;
|
||||||
|
ALTER TABLE token RENAME COLUMN ccn_ua TO ua;
|
||||||
|
ALTER TABLE token RENAME COLUMN ccn_ip TO ip;
|
||||||
|
|
||||||
|
-- Step 3: Rename collection table columns
|
||||||
|
ALTER TABLE collection RENAME COLUMN ccn_uuid TO uuid;
|
||||||
|
ALTER TABLE collection RENAME COLUMN ccn_name TO name;
|
||||||
|
ALTER TABLE collection RENAME COLUMN ccn_user TO user;
|
||||||
|
ALTER TABLE collection RENAME COLUMN ccn_lastChange TO last_change;
|
||||||
|
|
||||||
|
-- Step 4: Rename share table columns
|
||||||
|
ALTER TABLE share RENAME COLUMN ccn_uuid TO uuid;
|
||||||
|
ALTER TABLE share RENAME COLUMN ccn_target TO target;
|
||||||
|
|
||||||
|
-- Step 5: Rename calendar table columns
|
||||||
|
ALTER TABLE calendar RENAME COLUMN ccn_uuid TO uuid;
|
||||||
|
ALTER TABLE calendar RENAME COLUMN ccn_belongTo TO belong_to;
|
||||||
|
ALTER TABLE calendar RENAME COLUMN ccn_title TO title;
|
||||||
|
ALTER TABLE calendar RENAME COLUMN ccn_description TO description;
|
||||||
|
ALTER TABLE calendar RENAME COLUMN ccn_lastChange TO last_change;
|
||||||
|
ALTER TABLE calendar RENAME COLUMN ccn_eventDateTimeStart TO event_date_time_start;
|
||||||
|
ALTER TABLE calendar RENAME COLUMN ccn_eventDateTimeEnd TO event_date_time_end;
|
||||||
|
ALTER TABLE calendar RENAME COLUMN ccn_timezoneOffset TO timezone_offset;
|
||||||
|
ALTER TABLE calendar RENAME COLUMN ccn_loopRules TO loop_rules;
|
||||||
|
ALTER TABLE calendar RENAME COLUMN ccn_loopDateTimeStart TO loop_date_time_start;
|
||||||
|
ALTER TABLE calendar RENAME COLUMN ccn_loopDateTimeEnd TO loop_date_time_end;
|
||||||
|
|
||||||
|
-- Step 6: Rename todo table columns
|
||||||
|
ALTER TABLE todo RENAME COLUMN ccn_uuid TO uuid;
|
||||||
|
ALTER TABLE todo RENAME COLUMN ccn_belongTo TO belong_to;
|
||||||
|
ALTER TABLE todo RENAME COLUMN ccn_data TO data;
|
||||||
|
ALTER TABLE todo RENAME COLUMN ccn_lastChange TO last_change;
|
||||||
|
|
||||||
|
-- Note: Foreign key constraints will be automatically updated by SQLite when renaming columns
|
||||||
|
-- No additional steps needed for foreign keys
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -1,54 +1,81 @@
|
|||||||
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 Callable, ParamSpec, TypeVar, Generic
|
||||||
|
|
||||||
def SafeDatabaseOperation(func):
|
import dt
|
||||||
def wrapper(self: 'CalendarDatabase', *args, **kwargs):
|
import utils
|
||||||
|
import config
|
||||||
|
from logger import LOGGER
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar('T')
|
||||||
|
P = ParamSpec('P')
|
||||||
|
R = TypeVar('R')
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ResponseBody(Generic[T]):
|
||||||
|
"""The generic response body for API return."""
|
||||||
|
|
||||||
|
success: bool
|
||||||
|
"""True if this operation is successful, otherwise false."""
|
||||||
|
error: str
|
||||||
|
"""The error message provided when operation failed."""
|
||||||
|
data: T | None
|
||||||
|
"""The payload provided when operation successed."""
|
||||||
|
|
||||||
|
|
||||||
|
class DbException(Exception):
|
||||||
|
"""Error occurs when manipulating with database."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def SafeDatabaseOperation(inner: Callable[P, R]) -> Callable[P, ResponseBody[R]]:
|
||||||
|
def wrapper(*args, **kwargs) -> ResponseBody[R]:
|
||||||
|
# extract self from args
|
||||||
|
self: 'CalendarDatabase' = args[0]
|
||||||
|
# get config
|
||||||
cfg = config.get_config()
|
cfg = config.get_config()
|
||||||
|
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
# check database and acquire cursor
|
# try to fetching database and allocate database cursor
|
||||||
try:
|
try:
|
||||||
self.check_database()
|
db = self._get_db()
|
||||||
self.cursor = self.db.cursor()
|
self._allocate_cursor()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.cursor = None
|
self._free_cursor()
|
||||||
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, '', inner(*args, **kwargs))
|
||||||
self.cursor.close()
|
self._free_cursor()
|
||||||
self.cursor = None
|
db.commit()
|
||||||
self.db.commit()
|
|
||||||
return result
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.cursor.close()
|
self._free_cursor()
|
||||||
self.cursor = None
|
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
|
||||||
|
|
||||||
class CalendarDatabase:
|
class CalendarDatabase:
|
||||||
|
|
||||||
db: sqlite3.Connection
|
db: sqlite3.Connection | None
|
||||||
cursor: sqlite3.Cursor
|
cursor: sqlite3.Cursor | None
|
||||||
mutex: threading.Lock
|
mutex: threading.Lock
|
||||||
latestClean: int
|
latestClean: int
|
||||||
|
|
||||||
@@ -59,8 +86,8 @@ class CalendarDatabase:
|
|||||||
self.latestClean = 0
|
self.latestClean = 0
|
||||||
|
|
||||||
def open(self):
|
def open(self):
|
||||||
if (self.is_database_valid()):
|
if (self.db is not None):
|
||||||
raise Exception('Databade is opened')
|
raise DbException('Database is already opened')
|
||||||
|
|
||||||
cfg = config.get_config()
|
cfg = config.get_config()
|
||||||
match cfg.database.driver:
|
match cfg.database.driver:
|
||||||
@@ -69,13 +96,13 @@ class CalendarDatabase:
|
|||||||
self.db.execute('PRAGMA encoding = "UTF-8";')
|
self.db.execute('PRAGMA encoding = "UTF-8";')
|
||||||
self.db.execute('PRAGMA foreign_keys = ON;')
|
self.db.execute('PRAGMA foreign_keys = ON;')
|
||||||
case config.DatabaseDriver.MYSQL:
|
case config.DatabaseDriver.MYSQL:
|
||||||
raise Exception('Not implemented database')
|
raise DbException('Not implemented database')
|
||||||
case _:
|
case _:
|
||||||
raise Exception('Unknow database type')
|
raise DbException('Unknow database type')
|
||||||
|
|
||||||
def init(self, username, password):
|
def init(self, username: str, password: str):
|
||||||
if (self.is_database_valid()):
|
if (self.db is not None):
|
||||||
raise Exception('Database is opened')
|
raise DbException('Database is already opened')
|
||||||
|
|
||||||
# establish tables
|
# establish tables
|
||||||
cfg = config.get_config()
|
cfg = config.get_config()
|
||||||
@@ -85,44 +112,74 @@ class CalendarDatabase:
|
|||||||
case config.DatabaseDriver.SQLITE:
|
case config.DatabaseDriver.SQLITE:
|
||||||
sql_file = backend_sql_path / 'sqlite.sql'
|
sql_file = backend_sql_path / 'sqlite.sql'
|
||||||
case config.DatabaseDriver.MYSQL:
|
case config.DatabaseDriver.MYSQL:
|
||||||
raise Exception('Not implemented database')
|
raise DbException('Not implemented database')
|
||||||
case _:
|
case _:
|
||||||
raise Exception('Unknow database type')
|
raise DbException('Unknow database type')
|
||||||
|
|
||||||
self.open()
|
self.open()
|
||||||
cursor = self.db.cursor()
|
db = self._get_db()
|
||||||
|
|
||||||
|
self._allocate_cursor()
|
||||||
|
cursor = self._get_cursor()
|
||||||
|
|
||||||
|
# execute script for creating tables
|
||||||
with open(sql_file, 'r', encoding='utf-8') as fsql:
|
with open(sql_file, 'r', encoding='utf-8') as fsql:
|
||||||
cursor.executescript(fsql.read())
|
cursor.executescript(fsql.read())
|
||||||
|
# add default user in user table
|
||||||
# finish init
|
|
||||||
cursor.execute('INSERT INTO user VALUES (?, ?, ?, ?);', (
|
cursor.execute('INSERT INTO user VALUES (?, ?, ?, ?);', (
|
||||||
username,
|
username,
|
||||||
utils.ComputePasswordHash(password),
|
utils.ComputePasswordHash(password),
|
||||||
1,
|
1,
|
||||||
utils.GenerateSalt()
|
utils.GenerateSalt()
|
||||||
))
|
))
|
||||||
cursor.close()
|
|
||||||
self.db.commit()
|
self._free_cursor()
|
||||||
|
|
||||||
|
# commit to database
|
||||||
|
db.commit()
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
self.check_database()
|
if (self.db is None):
|
||||||
self.db.close()
|
LOGGER.warning('Try to close null database.')
|
||||||
self.db = None
|
else:
|
||||||
|
self._free_cursor()
|
||||||
|
self.db.close()
|
||||||
|
self.db = None
|
||||||
|
|
||||||
def check_database(self):
|
def _get_db(self) -> sqlite3.Connection:
|
||||||
if (not self.is_database_valid()):
|
if (self.db is None):
|
||||||
raise Exception('Databade is None')
|
raise DbException('There is no opened database')
|
||||||
|
else:
|
||||||
|
return self.db
|
||||||
|
|
||||||
def is_database_valid(self):
|
def _allocate_cursor(self) -> None:
|
||||||
return not (self.db == None)
|
if (self.cursor is not None):
|
||||||
|
raise DbException('There is already opened database cursor')
|
||||||
|
else:
|
||||||
|
self.cursor = self._get_db().cursor()
|
||||||
|
|
||||||
|
def _get_cursor(self) -> sqlite3.Cursor:
|
||||||
|
if (self.cursor is None):
|
||||||
|
raise DbException('There is no opened database cursor')
|
||||||
|
else:
|
||||||
|
return self.cursor
|
||||||
|
|
||||||
|
def _free_cursor(self) -> None:
|
||||||
|
if (self.cursor is None):
|
||||||
|
LOGGER.warning('Try to free null databse cursor.')
|
||||||
|
else:
|
||||||
|
self.cursor.close()
|
||||||
|
self.cursor = None
|
||||||
|
|
||||||
# ======================= token related internal operation
|
# ======================= token related internal operation
|
||||||
def tokenOper_clean(self):
|
def tokenOper_clean(self):
|
||||||
# remove outdated token
|
# remove outdated token
|
||||||
self.cursor.execute('DELETE FROM token WHERE [ccn_tokenExpireOn] <= ?',(utils.GetCurrentTimestamp(), ))
|
cursor = self._get_cursor()
|
||||||
|
cursor.execute('DELETE FROM token WHERE [token_expire_on] <= ?',(utils.GetCurrentTimestamp(), ))
|
||||||
|
|
||||||
def tokenOper_postpone_expireOn(self, token):
|
def tokenOper_postpone_expireOn(self, token):
|
||||||
self.cursor.execute('UPDATE token SET [ccn_tokenExpireOn] = ? WHERE [ccn_token] = ?;', (
|
cursor = self._get_cursor()
|
||||||
|
cursor.execute('UPDATE token SET [token_expire_on] = ? WHERE [token] = ?;', (
|
||||||
utils.GetTokenExpireOn(),
|
utils.GetTokenExpireOn(),
|
||||||
token
|
token
|
||||||
))
|
))
|
||||||
@@ -131,16 +188,18 @@ class CalendarDatabase:
|
|||||||
self.tokenOper_get_username(token)
|
self.tokenOper_get_username(token)
|
||||||
|
|
||||||
def tokenOper_is_admin(self, username):
|
def tokenOper_is_admin(self, username):
|
||||||
self.cursor.execute('SELECT [ccn_isAdmin] FROM user WHERE [ccn_name] = ?;',(username, ))
|
cursor = self._get_cursor()
|
||||||
cache = self.cursor.fetchone()[0]
|
cursor.execute('SELECT [is_admin] FROM user WHERE [name] = ?;',(username, ))
|
||||||
|
cache = cursor.fetchone()[0]
|
||||||
return cache == 1
|
return cache == 1
|
||||||
|
|
||||||
def tokenOper_get_username(self, token):
|
def tokenOper_get_username(self, token):
|
||||||
self.cursor.execute('SELECT [ccn_user] FROM token WHERE [ccn_token] = ? AND [ccn_tokenExpireOn] > ?;',(
|
cursor = self._get_cursor()
|
||||||
|
cursor.execute('SELECT [user] FROM token WHERE [token] = ? AND [token_expire_on] > ?;',(
|
||||||
token,
|
token,
|
||||||
utils.GetCurrentTimestamp()
|
utils.GetCurrentTimestamp()
|
||||||
))
|
))
|
||||||
result = self.cursor.fetchone()[0]
|
result = cursor.fetchone()[0]
|
||||||
# need postpone expire on time
|
# need postpone expire on time
|
||||||
self.tokenOper_postpone_expireOn(token)
|
self.tokenOper_postpone_expireOn(token)
|
||||||
return result
|
return result
|
||||||
@@ -150,8 +209,9 @@ class CalendarDatabase:
|
|||||||
|
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def common_salt(self, username):
|
def common_salt(self, username):
|
||||||
|
cursor = self._get_cursor()
|
||||||
salt = utils.GenerateSalt()
|
salt = utils.GenerateSalt()
|
||||||
self.cursor.execute('UPDATE user SET [ccn_salt] = ? WHERE [ccn_name] = ?;', (
|
cursor.execute('UPDATE user SET [salt] = ? WHERE [name] = ?;', (
|
||||||
salt,
|
salt,
|
||||||
username
|
username
|
||||||
))
|
))
|
||||||
@@ -159,16 +219,17 @@ class CalendarDatabase:
|
|||||||
|
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def common_login(self, username, password, clientUa, clientIp):
|
def common_login(self, username, password, clientUa, clientIp):
|
||||||
self.cursor.execute('SELECT [ccn_password], [ccn_salt] FROM user WHERE [ccn_name] = ?;', (username, ))
|
cursor = self._get_cursor()
|
||||||
(gotten_salt, gotten_password) = self.cursor.fetchone()
|
cursor.execute('SELECT [password], [salt] FROM user WHERE [name] = ?;', (username, ))
|
||||||
|
(gotten_salt, gotten_password) = cursor.fetchone()
|
||||||
|
|
||||||
if password == utils.ComputePasswordHashWithSalt(gotten_password, gotten_salt):
|
if password == utils.ComputePasswordHashWithSalt(gotten_password, gotten_salt):
|
||||||
token = utils.GenerateToken(username)
|
token = utils.GenerateToken(username)
|
||||||
self.cursor.execute('UPDATE user SET [ccn_salt] = ? WHERE [ccn_name] = ?;', (
|
cursor.execute('UPDATE user SET [salt] = ? WHERE [name] = ?;', (
|
||||||
utils.GenerateSalt(), # regenerate a new slat to prevent re-login try
|
utils.GenerateSalt(), # regenerate a new slat to prevent re-login try
|
||||||
username
|
username
|
||||||
))
|
))
|
||||||
self.cursor.execute('INSERT INTO token VALUES (?, ?, ?, ?, ?);', (
|
cursor.execute('INSERT INTO token VALUES (?, ?, ?, ?, ?);', (
|
||||||
username,
|
username,
|
||||||
token,
|
token,
|
||||||
utils.GetTokenExpireOn(), # add 2 day from now
|
utils.GetTokenExpireOn(), # add 2 day from now
|
||||||
@@ -178,15 +239,21 @@ class CalendarDatabase:
|
|||||||
return token
|
return token
|
||||||
else:
|
else:
|
||||||
# throw a exception to indicate fail to login
|
# throw a exception to indicate fail to login
|
||||||
raise Exception('Login authentication failed')
|
raise DbException('Login authentication failed')
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def common_webLogin(self, username, password, clientUa, clientIp):
|
def common_webLogin(self, username, password, clientUa, clientIp):
|
||||||
self.cursor.execute('SELECT [ccn_name] FROM user WHERE [ccn_name] = ? AND [ccn_password] = ?;', (username, utils.ComputePasswordHash(password)))
|
cursor = self._get_cursor()
|
||||||
|
LOGGER.debug(f'WebLogin Username: {username}')
|
||||||
|
LOGGER.debug(f'WebLogin Password: {password}')
|
||||||
|
passwordHash = utils.ComputePasswordHash(password)
|
||||||
|
LOGGER.debug(f'WebLogin Password Hash: {passwordHash}')
|
||||||
|
|
||||||
if len(self.cursor.fetchall()) != 0:
|
cursor.execute('SELECT [name] FROM user WHERE [name] = ? AND [password] = ?;', (username, passwordHash))
|
||||||
|
|
||||||
|
if len(cursor.fetchall()) != 0:
|
||||||
token = utils.GenerateToken(username)
|
token = utils.GenerateToken(username)
|
||||||
self.cursor.execute('INSERT INTO token VALUES (?, ?, ?, ?, ?);', (
|
cursor.execute('INSERT INTO token VALUES (?, ?, ?, ?, ?);', (
|
||||||
username,
|
username,
|
||||||
token,
|
token,
|
||||||
utils.GetTokenExpireOn(), # add 2 day from now
|
utils.GetTokenExpireOn(), # add 2 day from now
|
||||||
@@ -196,12 +263,13 @@ class CalendarDatabase:
|
|||||||
return token
|
return token
|
||||||
else:
|
else:
|
||||||
# throw a exception to indicate fail to login
|
# throw a exception to indicate fail to login
|
||||||
raise Exception('Login authentication failed')
|
raise DbException('Login authentication failed')
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def common_logout(self, token):
|
def common_logout(self, token):
|
||||||
|
cursor = self._get_cursor()
|
||||||
self.tokenOper_check_valid(token)
|
self.tokenOper_check_valid(token)
|
||||||
self.cursor.execute('DELETE FROM token WHERE [ccn_token] = ?;', (token, ))
|
cursor.execute('DELETE FROM token WHERE [token] = ?;', (token, ))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
@@ -212,40 +280,44 @@ class CalendarDatabase:
|
|||||||
# =============================== calendar
|
# =============================== calendar
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def calendar_getFull(self, token, startDateTime, endDateTime):
|
def calendar_getFull(self, token, startDateTime, endDateTime):
|
||||||
|
cursor = self._get_cursor()
|
||||||
username = self.tokenOper_get_username(token)
|
username = self.tokenOper_get_username(token)
|
||||||
self.cursor.execute('SELECT calendar.* FROM calendar INNER JOIN collection \
|
cursor.execute('SELECT calendar.* FROM calendar INNER JOIN collection \
|
||||||
ON collection.ccn_uuid = calendar.ccn_belongTo \
|
ON collection.uuid = calendar.belong_to \
|
||||||
WHERE (collection.ccn_user = ? AND calendar.ccn_loopDateTimeEnd >= ? AND calendar.ccn_loopDateTimeStart - (calendar.ccn_eventDateTimeEnd - calendar.ccn_eventDateTimeStart) <= ?);',
|
WHERE (collection.user = ? AND calendar.loop_date_time_end >= ? AND calendar.loop_date_time_start - (calendar.event_date_time_end - calendar.event_date_time_start) <= ?);',
|
||||||
(username, startDateTime, endDateTime))
|
(username, startDateTime, endDateTime))
|
||||||
return self.cursor.fetchall()
|
return cursor.fetchall()
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def calendar_getList(self, token, startDateTime, endDateTime):
|
def calendar_getList(self, token, startDateTime, endDateTime):
|
||||||
|
cursor = self._get_cursor()
|
||||||
username = self.tokenOper_get_username(token)
|
username = self.tokenOper_get_username(token)
|
||||||
self.cursor.execute('SELECT calendar.ccn_uuid FROM calendar INNER JOIN collection \
|
cursor.execute('SELECT calendar.uuid FROM calendar INNER JOIN collection \
|
||||||
ON collection.ccn_uuid = calendar.ccn_belongTo \
|
ON collection.uuid = calendar.belong_to \
|
||||||
WHERE (collection.ccn_user = ? AND calendar.ccn_loopDateTimeEnd >= ? AND calendar.ccn_loopDateTimeStart - (calendar.ccn_eventDateTimeEnd - calendar.ccn_eventDateTimeStart) <= ?);',
|
WHERE (collection.user = ? AND calendar.loop_date_time_end >= ? AND calendar.loop_date_time_start - (calendar.event_date_time_end - calendar.event_date_time_start) <= ?);',
|
||||||
(username, startDateTime, endDateTime))
|
(username, startDateTime, endDateTime))
|
||||||
return tuple(map(lambda x: x[0], self.cursor.fetchall()))
|
return tuple(map(lambda x: x[0], cursor.fetchall()))
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def calendar_getDetail(self, token, uuid):
|
def calendar_getDetail(self, token, uuid):
|
||||||
|
cursor = self._get_cursor()
|
||||||
self.tokenOper_check_valid(token)
|
self.tokenOper_check_valid(token)
|
||||||
self.cursor.execute('SELECT * FROM calendar WHERE [ccn_uuid] = ?;', (uuid, ))
|
cursor.execute('SELECT * FROM calendar WHERE [uuid] = ?;', (uuid, ))
|
||||||
return self.cursor.fetchone()
|
return cursor.fetchone()
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def calendar_update(self, token, uuid, lastChange, **optArgs):
|
def calendar_update(self, token, uuid, lastChange, **optArgs):
|
||||||
|
cursor = self._get_cursor()
|
||||||
self.tokenOper_check_valid(token)
|
self.tokenOper_check_valid(token)
|
||||||
|
|
||||||
# get prev data
|
# get prev data
|
||||||
self.cursor.execute('SELECT * FROM calendar WHERE [ccn_uuid] = ? AND [ccn_lastChange] = ?;', (uuid, lastChange))
|
cursor.execute('SELECT * FROM calendar WHERE [uuid] = ? AND [last_change] = ?;', (uuid, lastChange))
|
||||||
analyseData = list(self.cursor.fetchone())
|
analyseData = list(cursor.fetchone())
|
||||||
|
|
||||||
# construct update data
|
# construct update data
|
||||||
lastupdate = utils.GenerateUUID()
|
lastupdate = utils.GenerateUUID()
|
||||||
sqlList = [
|
sqlList = [
|
||||||
'[ccn_lastChange] = ?',
|
'[last_change] = ?',
|
||||||
]
|
]
|
||||||
argumentsList = [
|
argumentsList = [
|
||||||
lastupdate,
|
lastupdate,
|
||||||
@@ -256,44 +328,44 @@ class CalendarDatabase:
|
|||||||
|
|
||||||
cache = optArgs.get('belongTo', None)
|
cache = optArgs.get('belongTo', None)
|
||||||
if cache is not None:
|
if cache is not None:
|
||||||
sqlList.append('[ccn_belongTo] = ?')
|
sqlList.append('[belong_to] = ?')
|
||||||
argumentsList.append(cache)
|
argumentsList.append(cache)
|
||||||
cache = optArgs.get('title', None)
|
cache = optArgs.get('title', None)
|
||||||
if cache is not None:
|
if cache is not None:
|
||||||
sqlList.append('[ccn_title] = ?')
|
sqlList.append('[title] = ?')
|
||||||
argumentsList.append(cache)
|
argumentsList.append(cache)
|
||||||
cache = optArgs.get('description', None)
|
cache = optArgs.get('description', None)
|
||||||
if cache is not None:
|
if cache is not None:
|
||||||
sqlList.append('[ccn_description] = ?')
|
sqlList.append('[description] = ?')
|
||||||
argumentsList.append(cache)
|
argumentsList.append(cache)
|
||||||
cache = optArgs.get('eventDateTimeStart', None)
|
cache = optArgs.get('eventDateTimeStart', None)
|
||||||
if cache is not None:
|
if cache is not None:
|
||||||
sqlList.append('[ccn_eventDateTimeStart] = ?')
|
sqlList.append('[event_date_time_start] = ?')
|
||||||
argumentsList.append(cache)
|
argumentsList.append(cache)
|
||||||
reAnalyseLoop = True
|
reAnalyseLoop = True
|
||||||
analyseData[5] = cache
|
analyseData[5] = cache
|
||||||
cache = optArgs.get('eventDateTimeEnd', None)
|
cache = optArgs.get('eventDateTimeEnd', None)
|
||||||
if cache is not None:
|
if cache is not None:
|
||||||
sqlList.append('[ccn_eventDateTimeEnd] = ?')
|
sqlList.append('[event_date_time_end] = ?')
|
||||||
argumentsList.append(cache)
|
argumentsList.append(cache)
|
||||||
cache = optArgs.get('loopRules', None)
|
cache = optArgs.get('loopRules', None)
|
||||||
if cache is not None:
|
if cache is not None:
|
||||||
sqlList.append('[ccn_loopRules] = ?')
|
sqlList.append('[loop_rules] = ?')
|
||||||
argumentsList.append(cache)
|
argumentsList.append(cache)
|
||||||
reAnalyseLoop = True
|
reAnalyseLoop = True
|
||||||
analyseData[8] = cache
|
analyseData[8] = cache
|
||||||
cache = optArgs.get('timezoneOffset', None)
|
cache = optArgs.get('timezoneOffset', None)
|
||||||
if cache is not None:
|
if cache is not None:
|
||||||
sqlList.append('[ccn_timezoneOffset] = ?')
|
sqlList.append('[timezone_offset] = ?')
|
||||||
argumentsList.append(cache)
|
argumentsList.append(cache)
|
||||||
reAnalyseLoop = True
|
reAnalyseLoop = True
|
||||||
analyseData[7] = cache
|
analyseData[7] = cache
|
||||||
|
|
||||||
if reAnalyseLoop:
|
if reAnalyseLoop:
|
||||||
# re-compute loop data and upload it into list
|
# re-compute loop data and upload it into list
|
||||||
sqlList.append('[ccn_loopDateTimeStart] = ?')
|
sqlList.append('[loop_date_time_start] = ?')
|
||||||
argumentsList.append(analyseData[5])
|
argumentsList.append(analyseData[5])
|
||||||
sqlList.append('[ccn_loopDateTimeEnd] = ?')
|
sqlList.append('[loop_date_time_end] = ?')
|
||||||
argumentsList.append(str(dt.ResolveLoopStr(
|
argumentsList.append(str(dt.ResolveLoopStr(
|
||||||
analyseData[8],
|
analyseData[8],
|
||||||
analyseData[5],
|
analyseData[5],
|
||||||
@@ -302,14 +374,15 @@ class CalendarDatabase:
|
|||||||
|
|
||||||
# execute
|
# execute
|
||||||
argumentsList.append(uuid)
|
argumentsList.append(uuid)
|
||||||
self.cursor.execute('UPDATE calendar SET {} WHERE [ccn_uuid] = ?;'.format(', '.join(sqlList)),
|
cursor.execute('UPDATE calendar SET {} WHERE [uuid] = ?;'.format(', '.join(sqlList)),
|
||||||
tuple(argumentsList))
|
tuple(argumentsList))
|
||||||
if self.cursor.rowcount != 1:
|
if cursor.rowcount != 1:
|
||||||
raise Exception('Fail to update due to no matched rows or too much rows.')
|
raise DbException('Fail to update due to no matched rows or too much rows.')
|
||||||
return lastupdate
|
return lastupdate
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def calendar_add(self, token, belongTo, title, description, eventDateTimeStart, eventDateTimeEnd, loopRules, timezoneOffset):
|
def calendar_add(self, token, belongTo, title, description, eventDateTimeStart, eventDateTimeEnd, loopRules, timezoneOffset):
|
||||||
|
cursor = self._get_cursor()
|
||||||
self.tokenOper_check_valid(token)
|
self.tokenOper_check_valid(token)
|
||||||
|
|
||||||
newuuid = utils.GenerateUUID()
|
newuuid = utils.GenerateUUID()
|
||||||
@@ -319,7 +392,7 @@ class CalendarDatabase:
|
|||||||
loopDateTimeStart = eventDateTimeStart
|
loopDateTimeStart = eventDateTimeStart
|
||||||
loopDateTimeEnd = dt.ResolveLoopStr(loopRules, eventDateTimeStart, timezoneOffset)
|
loopDateTimeEnd = dt.ResolveLoopStr(loopRules, eventDateTimeStart, timezoneOffset)
|
||||||
|
|
||||||
self.cursor.execute('INSERT INTO calendar VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);',
|
cursor.execute('INSERT INTO calendar VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);',
|
||||||
(newuuid,
|
(newuuid,
|
||||||
belongTo,
|
belongTo,
|
||||||
title,
|
title,
|
||||||
@@ -335,134 +408,149 @@ class CalendarDatabase:
|
|||||||
|
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def calendar_delete(self, token, uuid, lastChange):
|
def calendar_delete(self, token, uuid, lastChange):
|
||||||
|
cursor = self._get_cursor()
|
||||||
self.tokenOper_check_valid(token)
|
self.tokenOper_check_valid(token)
|
||||||
self.cursor.execute('DELETE FROM calendar WHERE [ccn_uuid] = ? AND [ccn_lastChange] = ?;', (uuid, lastChange))
|
cursor.execute('DELETE FROM calendar WHERE [uuid] = ? AND [last_change] = ?;', (uuid, lastChange))
|
||||||
if self.cursor.rowcount != 1:
|
if cursor.rowcount != 1:
|
||||||
raise Exception('Fail to delete due to no matched rows or too much rows.')
|
raise DbException('Fail to delete due to no matched rows or too much rows.')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# =============================== collection
|
# =============================== collection
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def collection_getFullOwn(self, token):
|
def collection_getFullOwn(self, token):
|
||||||
|
cursor = self._get_cursor()
|
||||||
username = self.tokenOper_get_username(token)
|
username = self.tokenOper_get_username(token)
|
||||||
self.cursor.execute('SELECT [ccn_uuid], [ccn_name], [ccn_lastChange] FROM collection WHERE [ccn_user] = ?;', (username, ))
|
cursor.execute('SELECT [uuid], [name], [last_change] FROM collection WHERE [user] = ?;', (username, ))
|
||||||
return self.cursor.fetchall()
|
return cursor.fetchall()
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def collection_getListOwn(self, token):
|
def collection_getListOwn(self, token):
|
||||||
|
cursor = self._get_cursor()
|
||||||
username = self.tokenOper_get_username(token)
|
username = self.tokenOper_get_username(token)
|
||||||
self.cursor.execute('SELECT [ccn_uuid] FROM collection WHERE [ccn_user] = ?;', (username, ))
|
cursor.execute('SELECT [uuid] FROM collection WHERE [user] = ?;', (username, ))
|
||||||
return tuple(map(lambda x: x[0], self.cursor.fetchall()))
|
return tuple(map(lambda x: x[0], cursor.fetchall()))
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def collection_getDetailOwn(self, token, uuid):
|
def collection_getDetailOwn(self, token, uuid):
|
||||||
|
cursor = self._get_cursor()
|
||||||
username = self.tokenOper_get_username(token)
|
username = self.tokenOper_get_username(token)
|
||||||
self.cursor.execute('SELECT [ccn_uuid], [ccn_name], [ccn_lastChange] FROM collection WHERE [ccn_user] = ? AND [ccn_uuid] = ?;', (username, uuid))
|
cursor.execute('SELECT [uuid], [name], [last_change] FROM collection WHERE [user] = ? AND [uuid] = ?;', (username, uuid))
|
||||||
return self.cursor.fetchone()
|
return cursor.fetchone()
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def collection_addOwn(self, token, newname):
|
def collection_addOwn(self, token, newname):
|
||||||
|
cursor = self._get_cursor()
|
||||||
username = self.tokenOper_get_username(token)
|
username = self.tokenOper_get_username(token)
|
||||||
newuuid = utils.GenerateUUID()
|
newuuid = utils.GenerateUUID()
|
||||||
lastupdate = utils.GenerateUUID()
|
lastupdate = utils.GenerateUUID()
|
||||||
self.cursor.execute('INSERT INTO collection VALUES (?, ?, ?, ?);',
|
cursor.execute('INSERT INTO collection VALUES (?, ?, ?, ?);',
|
||||||
(newuuid, newname, username, lastupdate))
|
(newuuid, newname, username, lastupdate))
|
||||||
return newuuid
|
return newuuid
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def collection_updateOwn(self, token, uuid, newname, lastChange):
|
def collection_updateOwn(self, token, uuid, newname, lastChange):
|
||||||
|
cursor = self._get_cursor()
|
||||||
self.tokenOper_check_valid(token)
|
self.tokenOper_check_valid(token)
|
||||||
|
|
||||||
lastupdate = utils.GenerateUUID()
|
lastupdate = utils.GenerateUUID()
|
||||||
self.cursor.execute('UPDATE collection SET [ccn_name] = ?, [ccn_lastChange] = ? WHERE [ccn_uuid] = ? AND [ccn_lastChange] = ?;', (
|
cursor.execute('UPDATE collection SET [name] = ?, [last_change] = ? WHERE [uuid] = ? AND [last_change] = ?;', (
|
||||||
newname,
|
newname,
|
||||||
lastupdate,
|
lastupdate,
|
||||||
uuid,
|
uuid,
|
||||||
lastChange
|
lastChange
|
||||||
))
|
))
|
||||||
if self.cursor.rowcount != 1:
|
if cursor.rowcount != 1:
|
||||||
raise Exception('Fail to update due to no matched rows or too much rows.')
|
raise DbException('Fail to update due to no matched rows or too much rows.')
|
||||||
return lastupdate
|
return lastupdate
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def collection_deleteOwn(self, token, uuid, lastChange):
|
def collection_deleteOwn(self, token, uuid, lastChange):
|
||||||
|
cursor = self._get_cursor()
|
||||||
self.tokenOper_check_valid(token)
|
self.tokenOper_check_valid(token)
|
||||||
|
|
||||||
self.cursor.execute('DELETE FROM collection WHERE [ccn_uuid] = ? AND [ccn_lastChange] = ?;', (
|
cursor.execute('DELETE FROM collection WHERE [uuid] = ? AND [last_change] = ?;', (
|
||||||
uuid,
|
uuid,
|
||||||
lastChange
|
lastChange
|
||||||
))
|
))
|
||||||
if self.cursor.rowcount != 1:
|
if cursor.rowcount != 1:
|
||||||
raise Exception('Fail to delete due to no matched rows or too much rows.')
|
raise DbException('Fail to delete due to no matched rows or too much rows.')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def collection_getSharing(self, token, uuid):
|
def collection_getSharing(self, token, uuid):
|
||||||
|
cursor = self._get_cursor()
|
||||||
self.tokenOper_check_valid(token)
|
self.tokenOper_check_valid(token)
|
||||||
self.cursor.execute('SELECT [ccn_target] FROM share WHERE [ccn_uuid] = ?;', (uuid, ))
|
cursor.execute('SELECT [target] FROM share WHERE [uuid] = ?;', (uuid, ))
|
||||||
return tuple(map(lambda x: x[0], self.cursor.fetchall()))
|
return tuple(map(lambda x: x[0], cursor.fetchall()))
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def collection_deleteSharing(self, token, uuid, target, lastChange):
|
def collection_deleteSharing(self, token, uuid, target, lastChange):
|
||||||
|
cursor = self._get_cursor()
|
||||||
self.tokenOper_check_valid(token)
|
self.tokenOper_check_valid(token)
|
||||||
|
|
||||||
lastupdate = utils.GenerateUUID()
|
lastupdate = utils.GenerateUUID()
|
||||||
self.cursor.execute('UPDATE collection SET [ccn_lastChange] = ?, WHERE [ccn_uuid] = ? AND [ccn_lastChange] = ?;', (lastupdate, uuid, lastChange))
|
cursor.execute('UPDATE collection SET [last_change] = ?, WHERE [uuid] = ? AND [last_change] = ?;', (lastupdate, uuid, lastChange))
|
||||||
if self.cursor.rowcount != 1:
|
if cursor.rowcount != 1:
|
||||||
raise Exception('Fail to delete due to no matched rows or too much rows.')
|
raise DbException('Fail to delete due to no matched rows or too much rows.')
|
||||||
|
|
||||||
self.cursor.execute('DELETE FROM share WHERE [ccn_uuid] = ? AND [ccn_target] = ?;', (uuid, target))
|
cursor.execute('DELETE FROM share WHERE [uuid] = ? AND [target] = ?;', (uuid, target))
|
||||||
if self.cursor.rowcount != 1:
|
if cursor.rowcount != 1:
|
||||||
raise Exception('Fail to delete due to no matched rows or too much rows.')
|
raise DbException('Fail to delete due to no matched rows or too much rows.')
|
||||||
|
|
||||||
return lastupdate
|
return lastupdate
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def collection_addSharing(self, token, uuid, target, lastChange):
|
def collection_addSharing(self, token, uuid, target, lastChange):
|
||||||
|
cursor = self._get_cursor()
|
||||||
self.tokenOper_check_valid(token)
|
self.tokenOper_check_valid(token)
|
||||||
|
|
||||||
lastupdate = utils.GenerateUUID()
|
lastupdate = utils.GenerateUUID()
|
||||||
self.cursor.execute('UPDATE collection SET [ccn_lastChange] = ? WHERE [ccn_uuid] = ? AND [ccn_lastChange] = ?;', (lastupdate, uuid, lastChange))
|
cursor.execute('UPDATE collection SET [last_change] = ? WHERE [uuid] = ? AND [last_change] = ?;', (lastupdate, uuid, lastChange))
|
||||||
if self.cursor.rowcount != 1:
|
if cursor.rowcount != 1:
|
||||||
raise Exception('Fail to delete due to no matched rows or too much rows.')
|
raise DbException('Fail to delete due to no matched rows or too much rows.')
|
||||||
|
|
||||||
self.cursor.execute('SELECT * FROM share WHERE [ccn_uuid] = ? AND [ccn_target] = ?;', (uuid, target))
|
cursor.execute('SELECT * FROM share WHERE [uuid] = ? AND [target] = ?;', (uuid, target))
|
||||||
if len(self.cursor.fetchall()) != 0:
|
if len(cursor.fetchall()) != 0:
|
||||||
raise Exception('Fail to insert duplicated item.')
|
raise DbException('Fail to insert duplicated item.')
|
||||||
self.cursor.execute('INSERT INTO share VALUES (?, ?);', (uuid, target))
|
cursor.execute('INSERT INTO share VALUES (?, ?);', (uuid, target))
|
||||||
|
|
||||||
return lastupdate
|
return lastupdate
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def collection_getShared(self, token):
|
def collection_getShared(self, token):
|
||||||
|
cursor = self._get_cursor()
|
||||||
username = self.tokenOper_get_username(token)
|
username = self.tokenOper_get_username(token)
|
||||||
self.cursor.execute('SELECT collection.ccn_uuid, collection.ccn_name, collection.ccn_user \
|
cursor.execute('SELECT collection.uuid, collection.name, collection.user \
|
||||||
FROM share INNER JOIN collection \
|
FROM share INNER JOIN collection \
|
||||||
ON share.ccn_uuid = collection.ccn_uuid \
|
ON share.uuid = collection.uuid \
|
||||||
WHERE share.ccn_target = ?;', (username, ))
|
WHERE share.target = ?;', (username, ))
|
||||||
return self.cursor.fetchall()
|
return cursor.fetchall()
|
||||||
|
|
||||||
# =============================== todo
|
# =============================== todo
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def todo_getFull(self, token):
|
def todo_getFull(self, token):
|
||||||
|
cursor = self._get_cursor()
|
||||||
username = self.tokenOper_get_username(token)
|
username = self.tokenOper_get_username(token)
|
||||||
self.cursor.execute('SELECT * FROM todo WHERE [ccn_belongTo] = ?;', (username, ))
|
cursor.execute('SELECT * FROM todo WHERE [belong_to] = ?;', (username, ))
|
||||||
return self.cursor.fetchall()
|
return cursor.fetchall()
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def todo_getList(self, token):
|
def todo_getList(self, token):
|
||||||
|
cursor = self._get_cursor()
|
||||||
username = self.tokenOper_get_username(token)
|
username = self.tokenOper_get_username(token)
|
||||||
self.cursor.execute('SELECT [ccn_uuid] FROM todo WHERE [ccn_belongTo] = ?;', (username, ))
|
cursor.execute('SELECT [uuid] FROM todo WHERE [belong_to] = ?;', (username, ))
|
||||||
return tuple(map(lambda x: x[0], self.cursor.fetchall()))
|
return tuple(map(lambda x: x[0], cursor.fetchall()))
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def todo_getDetail(self, token, uuid):
|
def todo_getDetail(self, token, uuid):
|
||||||
|
cursor = self._get_cursor()
|
||||||
username = self.tokenOper_get_username(token)
|
username = self.tokenOper_get_username(token)
|
||||||
self.cursor.execute('SELECT * FROM todo WHERE [ccn_belongTo] = ? AND [ccn_uuid] = ?;', (username, uuid))
|
cursor.execute('SELECT * FROM todo WHERE [belong_to] = ? AND [uuid] = ?;', (username, uuid))
|
||||||
return self.cursor.fetchone()
|
return cursor.fetchone()
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def todo_add(self, token):
|
def todo_add(self, token):
|
||||||
|
cursor = self._get_cursor()
|
||||||
username = self.tokenOper_get_username(token)
|
username = self.tokenOper_get_username(token)
|
||||||
newuuid = utils.GenerateUUID()
|
newuuid = utils.GenerateUUID()
|
||||||
lastupdate = utils.GenerateUUID()
|
lastupdate = utils.GenerateUUID()
|
||||||
@@ -472,56 +560,60 @@ class CalendarDatabase:
|
|||||||
'',
|
'',
|
||||||
lastupdate,
|
lastupdate,
|
||||||
)
|
)
|
||||||
self.cursor.execute('INSERT INTO todo VALUES (?, ?, ?, ?);', returnedData)
|
cursor.execute('INSERT INTO todo VALUES (?, ?, ?, ?);', returnedData)
|
||||||
return returnedData
|
return returnedData
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def todo_update(self, token, uuid, data, lastChange):
|
def todo_update(self, token, uuid, data, lastChange):
|
||||||
|
cursor = self._get_cursor()
|
||||||
# check valid token
|
# check valid token
|
||||||
self.tokenOper_check_valid(token)
|
self.tokenOper_check_valid(token)
|
||||||
|
|
||||||
# update
|
# update
|
||||||
newLastChange = utils.GenerateUUID()
|
newLastChange = utils.GenerateUUID()
|
||||||
self.cursor.execute('UPDATE todo SET [ccn_data] = ?, [ccn_lastChange] = ? WHERE [ccn_uuid] = ? AND [ccn_lastChange] = ?;', (
|
cursor.execute('UPDATE todo SET [data] = ?, [last_change] = ? WHERE [uuid] = ? AND [last_change] = ?;', (
|
||||||
data,
|
data,
|
||||||
newLastChange,
|
newLastChange,
|
||||||
uuid,
|
uuid,
|
||||||
lastChange
|
lastChange
|
||||||
))
|
))
|
||||||
if self.cursor.rowcount != 1:
|
if cursor.rowcount != 1:
|
||||||
raise Exception('Fail to update due to no matched rows or too much rows.')
|
raise DbException('Fail to update due to no matched rows or too much rows.')
|
||||||
return newLastChange
|
return newLastChange
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def todo_delete(self, token, uuid, lastChange):
|
def todo_delete(self, token, uuid, lastChange):
|
||||||
|
cursor = self._get_cursor()
|
||||||
# check valid token
|
# check valid token
|
||||||
self.tokenOper_check_valid(token)
|
self.tokenOper_check_valid(token)
|
||||||
|
|
||||||
# delete
|
# delete
|
||||||
self.cursor.execute('DELETE FROM todo WHERE [ccn_uuid] = ? AND [ccn_lastChange] = ?;', (uuid, lastChange))
|
cursor.execute('DELETE FROM todo WHERE [uuid] = ? AND [last_change] = ?;', (uuid, lastChange))
|
||||||
if self.cursor.rowcount != 1:
|
if cursor.rowcount != 1:
|
||||||
raise Exception('Fail to delete due to no matched rows or too much rows.')
|
raise DbException('Fail to delete due to no matched rows or too much rows.')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
# =============================== admin
|
# =============================== admin
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def admin_get(self, token):
|
def admin_get(self, token):
|
||||||
|
cursor = self._get_cursor()
|
||||||
username = self.tokenOper_get_username(token)
|
username = self.tokenOper_get_username(token)
|
||||||
if not self.tokenOper_is_admin(username):
|
if not self.tokenOper_is_admin(username):
|
||||||
raise Exception('Permission denied.')
|
raise DbException('Permission denied.')
|
||||||
|
|
||||||
self.cursor.execute('SELECT [ccn_name], [ccn_isAdmin] FROM user;')
|
cursor.execute('SELECT [name], [is_admin] FROM user;')
|
||||||
return tuple(map(lambda x: (x[0], x[1] == 1), self.cursor.fetchall()))
|
return tuple(map(lambda x: (x[0], x[1] == 1), cursor.fetchall()))
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def admin_add(self, token, newname):
|
def admin_add(self, token, newname):
|
||||||
|
cursor = self._get_cursor()
|
||||||
username = self.tokenOper_get_username(token)
|
username = self.tokenOper_get_username(token)
|
||||||
if not self.tokenOper_is_admin(username):
|
if not self.tokenOper_is_admin(username):
|
||||||
raise Exception('Permission denied.')
|
raise DbException('Permission denied.')
|
||||||
|
|
||||||
newpassword = utils.ComputePasswordHash(utils.GenerateUUID())
|
newpassword = utils.ComputePasswordHash(utils.GenerateUUID())
|
||||||
self.cursor.execute('INSERT INTO user VALUES (?, ?, ?, ?);', (
|
cursor.execute('INSERT INTO user VALUES (?, ?, ?, ?);', (
|
||||||
newname,
|
newname,
|
||||||
newpassword,
|
newpassword,
|
||||||
0,
|
0,
|
||||||
@@ -531,9 +623,10 @@ class CalendarDatabase:
|
|||||||
|
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def admin_update(self, token, _username, **optArgs):
|
def admin_update(self, token, _username, **optArgs):
|
||||||
|
cursor = self._get_cursor()
|
||||||
username = self.tokenOper_get_username(token)
|
username = self.tokenOper_get_username(token)
|
||||||
if not self.tokenOper_is_admin(username):
|
if not self.tokenOper_is_admin(username):
|
||||||
raise Exception('Permission denied.')
|
raise DbException('Permission denied.')
|
||||||
|
|
||||||
# construct data
|
# construct data
|
||||||
sqlList = []
|
sqlList = []
|
||||||
@@ -542,45 +635,48 @@ class CalendarDatabase:
|
|||||||
# analyse opt arg
|
# analyse opt arg
|
||||||
cache = optArgs.get('password', None)
|
cache = optArgs.get('password', None)
|
||||||
if cache is not None:
|
if cache is not None:
|
||||||
sqlList.append('[ccn_password] = ?')
|
sqlList.append('[password] = ?')
|
||||||
argumentsList.append(utils.ComputePasswordHash(cache))
|
argumentsList.append(utils.ComputePasswordHash(cache))
|
||||||
cache = optArgs.get('isAdmin', None)
|
cache = optArgs.get('isAdmin', None)
|
||||||
if cache is not None:
|
if cache is not None:
|
||||||
sqlList.append('[ccn_isAdmin] = ?')
|
sqlList.append('[is_admin] = ?')
|
||||||
argumentsList.append(1 if cache else 0)
|
argumentsList.append(1 if cache else 0)
|
||||||
|
|
||||||
# execute
|
# execute
|
||||||
argumentsList.append(_username)
|
argumentsList.append(_username)
|
||||||
self.cursor.execute('UPDATE user SET {} WHERE [ccn_name] = ?;'.format(', '.join(sqlList)),
|
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 cursor.rowcount != 1:
|
||||||
raise Exception('Fail to update due to no matched rows or too much rows.')
|
raise DbException('Fail to update due to no matched rows or too much rows.')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def admin_delete(self, token, username):
|
def admin_delete(self, token, username):
|
||||||
|
cursor = self._get_cursor()
|
||||||
_username = self.tokenOper_get_username(token)
|
_username = self.tokenOper_get_username(token)
|
||||||
if not self.tokenOper_is_admin(_username):
|
if not self.tokenOper_is_admin(_username):
|
||||||
raise Exception('Permission denied.')
|
raise DbException('Permission denied.')
|
||||||
|
|
||||||
# delete
|
# delete
|
||||||
self.cursor.execute('DELETE FROM user WHERE [ccn_name] = ?;', (username, ))
|
cursor.execute('DELETE FROM user WHERE [name] = ?;', (username, ))
|
||||||
if self.cursor.rowcount != 1:
|
if cursor.rowcount != 1:
|
||||||
raise Exception('Fail to delete due to no matched rows or too much rows.')
|
raise DbException('Fail to delete due to no matched rows or too much rows.')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# =============================== profile
|
# =============================== profile
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def profile_isAdmin(self, token):
|
def profile_isAdmin(self, token):
|
||||||
|
cursor = self._get_cursor()
|
||||||
username = self.tokenOper_get_username(token)
|
username = self.tokenOper_get_username(token)
|
||||||
return self.tokenOper_is_admin(username)
|
return self.tokenOper_is_admin(username)
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def profile_changePassword(self, token, newpassword):
|
def profile_changePassword(self, token, newpassword):
|
||||||
|
cursor = self._get_cursor()
|
||||||
username = self.tokenOper_get_username(token)
|
username = self.tokenOper_get_username(token)
|
||||||
self.cursor.execute('UPDATE user SET [ccn_password] = ? WHERE [ccn_name] = ?;', (
|
cursor.execute('UPDATE user SET [password] = ? WHERE [name] = ?;', (
|
||||||
utils.ComputePasswordHash(newpassword),
|
utils.ComputePasswordHash(newpassword),
|
||||||
username
|
username
|
||||||
))
|
))
|
||||||
@@ -588,23 +684,25 @@ class CalendarDatabase:
|
|||||||
|
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def profile_getToken(self, token):
|
def profile_getToken(self, token):
|
||||||
|
cursor = self._get_cursor()
|
||||||
username = self.tokenOper_get_username(token)
|
username = self.tokenOper_get_username(token)
|
||||||
|
|
||||||
self.cursor.execute('SELECT * FROM token WHERE [ccn_user] = ?;', (
|
cursor.execute('SELECT * FROM token WHERE [user] = ?;', (
|
||||||
username,
|
username,
|
||||||
))
|
))
|
||||||
return self.cursor.fetchall()
|
return cursor.fetchall()
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
@SafeDatabaseOperation
|
||||||
def profile_deleteToken(self, token, deleteToken):
|
def profile_deleteToken(self, token, deleteToken):
|
||||||
|
cursor = self._get_cursor()
|
||||||
_username = self.tokenOper_get_username(token)
|
_username = self.tokenOper_get_username(token)
|
||||||
|
|
||||||
# delete
|
# delete
|
||||||
self.cursor.execute('DELETE FROM token WHERE [ccn_user] = ? AND [ccn_token] = ?;', (
|
cursor.execute('DELETE FROM token WHERE [user] = ? AND [token] = ?;', (
|
||||||
_username,
|
_username,
|
||||||
deleteToken
|
deleteToken
|
||||||
))
|
))
|
||||||
if self.cursor.rowcount != 1:
|
if cursor.rowcount != 1:
|
||||||
raise Exception('Fail to delete due to no matched rows or too much rows.')
|
raise DbException('Fail to delete due to no matched rows or too much rows.')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
42
backend/logger.py
Normal file
42
backend/logger.py
Normal 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)
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
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, ParamSpec, TypeVar, Generic
|
||||||
|
|
||||||
import config
|
import config
|
||||||
import database
|
import database
|
||||||
import utils
|
import utils
|
||||||
|
from logger import LOGGER
|
||||||
|
from database import ResponseBody
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
calendar_db = database.CalendarDatabase()
|
calendar_db = database.CalendarDatabase()
|
||||||
@@ -11,360 +16,498 @@ calendar_db = database.CalendarDatabase()
|
|||||||
|
|
||||||
# region: Common
|
# region: Common
|
||||||
|
|
||||||
@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(
|
||||||
(('username', str, False), ),
|
calendar_db.common_salt, (FormField("username", str, False),), None
|
||||||
None)
|
)
|
||||||
|
|
||||||
@app.route('/common/login', methods=['POST'])
|
|
||||||
|
@app.route("/common/login", methods=["POST"])
|
||||||
def api_common_loginHandle():
|
def api_common_loginHandle():
|
||||||
# construct client data first
|
clientInfo = FetchClientNetworkInfo()
|
||||||
clientUa = request.user_agent.string
|
|
||||||
if request.headers.getlist("X-Forwarded-For"):
|
|
||||||
clientIp = request.headers.getlist("X-Forwarded-For")[0]
|
|
||||||
else:
|
|
||||||
clientIp = request.remote_addr
|
|
||||||
|
|
||||||
return SmartDbCaller(calendar_db.common_login,
|
return SmartDbCaller(
|
||||||
(('username', str, False),
|
calendar_db.common_login,
|
||||||
('password', str, False),
|
(
|
||||||
('clientUa', str, False),
|
FormField("username", str, False),
|
||||||
('clientIp', str, False)),
|
FormField("password", str, False),
|
||||||
{
|
FormField("clientUa", str, False),
|
||||||
'clientUa': clientUa,
|
FormField("clientIp", str, False),
|
||||||
'clientIp': clientIp
|
),
|
||||||
})
|
{"clientUa": clientInfo.user_agent, "clientIp": clientInfo.ip_addr},
|
||||||
|
)
|
||||||
|
|
||||||
@app.route('/common/webLogin', methods=['POST'])
|
|
||||||
|
@app.route("/common/webLogin", methods=["POST"])
|
||||||
def api_common_webLoginHandle():
|
def api_common_webLoginHandle():
|
||||||
# construct client data first
|
clientInfo = FetchClientNetworkInfo()
|
||||||
clientUa = request.user_agent.string
|
|
||||||
if request.headers.getlist("X-Forwarded-For"):
|
|
||||||
clientIp = request.headers.getlist("X-Forwarded-For")[0]
|
|
||||||
else:
|
|
||||||
clientIp = request.remote_addr
|
|
||||||
|
|
||||||
return SmartDbCaller(calendar_db.common_webLogin,
|
return SmartDbCaller(
|
||||||
(('username', str, False),
|
calendar_db.common_webLogin,
|
||||||
('password', str, False),
|
(
|
||||||
('clientUa', str, False),
|
FormField("username", str, False),
|
||||||
('clientIp', str, False)),
|
FormField("password", str, False),
|
||||||
{
|
FormField("clientUa", str, False),
|
||||||
'clientUa': clientUa,
|
FormField("clientIp", str, False),
|
||||||
'clientIp': clientIp
|
),
|
||||||
})
|
{"clientUa": clientInfo.user_agent, "clientIp": clientInfo.ip_addr},
|
||||||
|
)
|
||||||
|
|
||||||
@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(
|
||||||
(('token', str, False), ),
|
calendar_db.common_logout, (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(
|
||||||
(('token', str, False), ),
|
calendar_db.common_tokenValid, (FormField("token", str, False),), None
|
||||||
None)
|
)
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# region: Calendar
|
# region: Calendar
|
||||||
|
|
||||||
@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(
|
||||||
(('token', str, False),
|
calendar_db.calendar_getFull,
|
||||||
('startDateTime', int, False),
|
(
|
||||||
('endDateTime', int, False)),
|
FormField("token", str, False),
|
||||||
None)
|
FormField("startDateTime", int, False),
|
||||||
|
FormField("endDateTime", int, False),
|
||||||
|
),
|
||||||
|
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(
|
||||||
(('token', str, False),
|
calendar_db.calendar_getList,
|
||||||
('startDateTime', int, False),
|
(
|
||||||
('endDateTime', int, False)),
|
FormField("token", str, False),
|
||||||
None)
|
FormField("startDateTime", int, False),
|
||||||
|
FormField("endDateTime", int, False),
|
||||||
|
),
|
||||||
|
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(
|
||||||
(('token', str, False),
|
calendar_db.calendar_getDetail,
|
||||||
('uuid', str, False)),
|
(FormField("token", 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(
|
||||||
(('token', str, False),
|
calendar_db.calendar_update,
|
||||||
('uuid', str, False),
|
(
|
||||||
('belongTo', str, True),
|
FormField("token", str, False),
|
||||||
('title', str, True),
|
FormField("uuid", str, False),
|
||||||
('description', str, True),
|
FormField("belongTo", str, True),
|
||||||
('eventDateTimeStart', int, True),
|
FormField("title", str, True),
|
||||||
('eventDateTimeEnd', int, True),
|
FormField("description", str, True),
|
||||||
('loopRules', str, True),
|
FormField("eventDateTimeStart", int, True),
|
||||||
('timezoneOffset', int, True),
|
FormField("eventDateTimeEnd", int, True),
|
||||||
('lastChange', str, False)),
|
FormField("loopRules", str, True),
|
||||||
None)
|
FormField("timezoneOffset", int, True),
|
||||||
|
FormField("lastChange", str, False),
|
||||||
|
),
|
||||||
|
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(
|
||||||
(('token', str, False),
|
calendar_db.calendar_add,
|
||||||
('belongTo', str, False),
|
(
|
||||||
('title', str, False),
|
FormField("token", str, False),
|
||||||
('description', str, False),
|
FormField("belongTo", str, False),
|
||||||
('eventDateTimeStart', int, False),
|
FormField("title", str, False),
|
||||||
('eventDateTimeEnd', int, False),
|
FormField("description", str, False),
|
||||||
('loopRules', str, False),
|
FormField("eventDateTimeStart", int, False),
|
||||||
('timezoneOffset', int, False)),
|
FormField("eventDateTimeEnd", int, False),
|
||||||
None)
|
FormField("loopRules", str, False),
|
||||||
|
FormField("timezoneOffset", int, False),
|
||||||
|
),
|
||||||
|
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(
|
||||||
(('token', str, False),
|
calendar_db.calendar_delete,
|
||||||
('uuid', str, False),
|
(
|
||||||
('lastChange', str, False)),
|
FormField("token", str, False),
|
||||||
None)
|
FormField("uuid", str, False),
|
||||||
|
FormField("lastChange", str, False),
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# region: Collection
|
# region: Collection
|
||||||
|
|
||||||
@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(
|
||||||
(('token', str, False), ),
|
calendar_db.collection_getFullOwn, (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(
|
||||||
(('token', str, False), ),
|
calendar_db.collection_getListOwn, (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(
|
||||||
(('token', str, False),
|
calendar_db.collection_getDetailOwn,
|
||||||
('uuid', str, False)),
|
(FormField("token", 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(
|
||||||
(('token', str, False),
|
calendar_db.collection_addOwn,
|
||||||
('name', str, False)),
|
(FormField("token", 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(
|
||||||
(('token', str, False),
|
calendar_db.collection_updateOwn,
|
||||||
('uuid', str, False),
|
(
|
||||||
('name', str, False),
|
FormField("token", str, False),
|
||||||
('lastChange', str, False)),
|
FormField("uuid", str, False),
|
||||||
None)
|
FormField("name", str, False),
|
||||||
|
FormField("lastChange", str, False),
|
||||||
|
),
|
||||||
|
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(
|
||||||
(('token', str, False),
|
calendar_db.collection_deleteOwn,
|
||||||
('uuid', str, False),
|
(
|
||||||
('lastChange', str, False)),
|
FormField("token", str, False),
|
||||||
None)
|
FormField("uuid", str, False),
|
||||||
|
FormField("lastChange", str, False),
|
||||||
|
),
|
||||||
|
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(
|
||||||
(('token', str, False),
|
calendar_db.collection_getSharing,
|
||||||
('uuid', str, False)),
|
(FormField("token", 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(
|
||||||
(('token', str, False),
|
calendar_db.collection_deleteSharing,
|
||||||
('uuid', str, False),
|
(
|
||||||
('target', str, False),
|
FormField("token", str, False),
|
||||||
('lastChange', str, False)),
|
FormField("uuid", str, False),
|
||||||
None)
|
FormField("target", str, False),
|
||||||
|
FormField("lastChange", str, False),
|
||||||
|
),
|
||||||
|
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(
|
||||||
(('token', str, False),
|
calendar_db.collection_addSharing,
|
||||||
('uuid', str, False),
|
(
|
||||||
('target', str, False),
|
FormField("token", str, False),
|
||||||
('lastChange', str, False)),
|
FormField("uuid", str, False),
|
||||||
None)
|
FormField("target", str, False),
|
||||||
|
FormField("lastChange", str, False),
|
||||||
|
),
|
||||||
|
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(
|
||||||
(('token', str, False), ),
|
calendar_db.collection_getShared, (FormField("token", str, False),), None
|
||||||
None)
|
)
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# region: Todo
|
# region: Todo
|
||||||
|
|
||||||
@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(
|
||||||
(('token', str, False), ),
|
calendar_db.todo_getFull, (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(
|
||||||
(('token', str, False), ),
|
calendar_db.todo_getList, (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(
|
||||||
(('token', str, False),
|
calendar_db.todo_getDetail,
|
||||||
('uuid', str, False)),
|
(FormField("token", 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, (FormField("token", str, False),), None)
|
||||||
(('token', str, False), ),
|
|
||||||
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(
|
||||||
(('token', str, False),
|
calendar_db.todo_update,
|
||||||
('uuid', str, False),
|
(
|
||||||
('data', str, False),
|
FormField("token", str, False),
|
||||||
('lastChange', str, False)),
|
FormField("uuid", str, False),
|
||||||
None)
|
FormField("data", str, False),
|
||||||
|
FormField("lastChange", str, False),
|
||||||
|
),
|
||||||
|
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(
|
||||||
(('token', str, False),
|
calendar_db.todo_delete,
|
||||||
('uuid', str, False),
|
(
|
||||||
('lastChange', str, False)),
|
FormField("token", str, False),
|
||||||
None)
|
FormField("uuid", str, False),
|
||||||
|
FormField("lastChange", str, False),
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# region: Admin
|
# region: Admin
|
||||||
|
|
||||||
@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, (FormField("token", str, False),), None)
|
||||||
(('token', str, False), ),
|
|
||||||
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(
|
||||||
(('token', str, False),
|
calendar_db.admin_add,
|
||||||
('username', str, False)),
|
(FormField("token", 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(
|
||||||
(('token', str, False),
|
calendar_db.admin_update,
|
||||||
('username', str, False),
|
(
|
||||||
('password', str, True),
|
FormField("token", str, False),
|
||||||
('isAdmin', utils.Str2Bool, True)),
|
FormField("username", str, False),
|
||||||
None)
|
FormField("password", str, True),
|
||||||
|
FormField("isAdmin", utils.Str2Bool, True),
|
||||||
|
),
|
||||||
|
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(
|
||||||
(('token', str, False),
|
calendar_db.admin_delete,
|
||||||
('username', str, False)),
|
(FormField("token", str, False), FormField("username", str, False)),
|
||||||
None)
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# region: Profile
|
# region: Profile
|
||||||
|
|
||||||
@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(
|
||||||
(('token', str, False), ),
|
calendar_db.profile_isAdmin, (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(
|
||||||
(('token', str, False),
|
calendar_db.profile_changePassword,
|
||||||
('password', str, False)),
|
(FormField("token", 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(
|
||||||
(('token', str, False), ),
|
calendar_db.profile_getToken, (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(
|
||||||
(('token', str, False),
|
calendar_db.profile_deleteToken,
|
||||||
('deleteToken', str, False)),
|
(FormField("token", str, False), FormField("deleteToken", str, False)),
|
||||||
None)
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# region: Misc Functions
|
# region: Utilities
|
||||||
|
|
||||||
def SmartDbCaller(dbMethod, paramTuple, extraDict):
|
|
||||||
result = (False, 'Invalid parameter', None)
|
@dataclass(frozen=True)
|
||||||
optCount = 0
|
class ClientNetworkInfo:
|
||||||
paramList = []
|
user_agent: str
|
||||||
optParamDict = {}
|
"""The user agent of client."""
|
||||||
# for each item,
|
ip_addr: str
|
||||||
# item[0] is field name.
|
"""The IP address of client."""
|
||||||
# item[1] is type.
|
|
||||||
# item[2] is whether it is optional field
|
|
||||||
realForm = request.form.to_dict()
|
def FetchClientNetworkInfo() -> ClientNetworkInfo:
|
||||||
if extraDict is not None:
|
clientUa = request.user_agent.string
|
||||||
realForm.update(extraDict)
|
forwardIpList = request.headers.getlist("X-Forwarded-For")
|
||||||
for item in paramTuple:
|
if forwardIpList:
|
||||||
cache = item[1](realForm.get(item[0], None))
|
clientIp = forwardIpList[0]
|
||||||
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:
|
else:
|
||||||
# at least one opt param
|
directIp = request.remote_addr
|
||||||
if optCount == 0 or len(optParamDict) != 0:
|
if directIp is not None:
|
||||||
result = dbMethod(*paramList, **optParamDict)
|
clientIp = directIp
|
||||||
|
else:
|
||||||
|
clientIp = "0.0.0.0"
|
||||||
|
|
||||||
|
return ClientNetworkInfo(clientUa, clientIp)
|
||||||
|
|
||||||
|
|
||||||
|
@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[..., ResponseBody[Any]],
|
||||||
|
fields: tuple[FormField, ...],
|
||||||
|
padding_form: dict[str, str] | None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
opt_param_counter = 0
|
||||||
|
lost_required: bool = False
|
||||||
|
param_list: list[Any] = []
|
||||||
|
opt_param_dict: dict[str, Any] = {}
|
||||||
|
|
||||||
|
# fetch user passed form
|
||||||
|
user_form: dict[str, str] = request.form.to_dict()
|
||||||
|
LOGGER.debug(f"User Form: {user_form}")
|
||||||
|
# overwrite user form by our padding form
|
||||||
|
if padding_form is not None:
|
||||||
|
user_form.update(padding_form)
|
||||||
|
LOGGER.debug(f"Padded User Form: {user_form}")
|
||||||
|
|
||||||
|
# check fields one by one
|
||||||
|
for field in fields:
|
||||||
|
value = user_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:
|
||||||
|
lost_required = True
|
||||||
|
else:
|
||||||
|
param_list.append(value)
|
||||||
|
|
||||||
|
# Only execute database function if there is no lost required fields.
|
||||||
|
# And fulfill one of following requirements:
|
||||||
|
# 1. There are all required fields (optional parameter count is zero).
|
||||||
|
# 1. Or, there is some optional parameter.
|
||||||
|
LOGGER.debug(f"Has Lost Required Parameter: {lost_required}")
|
||||||
|
LOGGER.debug(f"All Optional Parameter Count: {opt_param_counter}")
|
||||||
|
LOGGER.debug(f"Available Optional Parameter Count: {len(opt_param_dict)}")
|
||||||
|
result: ResponseBody[Any]
|
||||||
|
if lost_required == False and (opt_param_counter == 0 or len(opt_param_dict) != 0):
|
||||||
|
result = db_method(*param_list, **opt_param_dict)
|
||||||
|
else:
|
||||||
|
result = ResponseBody(False, "Invalid parameter", None)
|
||||||
|
|
||||||
return ConstructResponseBody(result)
|
return ConstructResponseBody(result)
|
||||||
|
|
||||||
def ConstructResponseBody(returnedTuple):
|
|
||||||
return {
|
def ConstructResponseBody(body: ResponseBody[Any]) -> dict[str, Any]:
|
||||||
'success': returnedTuple[0],
|
return {"success": body.success, "error": body.error, "data": body.data}
|
||||||
'error': returnedTuple[1],
|
|
||||||
'data': returnedTuple[2]
|
|
||||||
}
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
def run():
|
def run():
|
||||||
calendar_db.open()
|
calendar_db.open()
|
||||||
app.run(port=config.get_config().web.port)
|
app.run(port=config.get_config().web.port)
|
||||||
calendar_db.close()
|
calendar_db.close()
|
||||||
|
|
||||||
# endregion
|
|
||||||
|
|||||||
@@ -1,67 +1,67 @@
|
|||||||
CREATE TABLE user(
|
CREATE TABLE user(
|
||||||
[ccn_name] TEXT NOT NULL,
|
[name] TEXT NOT NULL,
|
||||||
[ccn_password] TEXT NOT NULL,
|
[password] TEXT NOT NULL,
|
||||||
[ccn_isAdmin] TINYINT NOT NULL CHECK(ccn_isAdmin = 1 OR ccn_isAdmin = 0),
|
[is_admin] TINYINT NOT NULL CHECK(is_admin = 1 OR is_admin = 0),
|
||||||
[ccn_salt] INTEGER NOT NULL,
|
[salt] INTEGER NOT NULL,
|
||||||
|
|
||||||
PRIMARY KEY (ccn_name)
|
PRIMARY KEY (name)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE token(
|
CREATE TABLE token(
|
||||||
[ccn_user] TEXT NOT NULL,
|
[user] TEXT NOT NULL,
|
||||||
[ccn_token] TEXT UNIQUE NOT NULL,
|
[token] TEXT UNIQUE NOT NULL,
|
||||||
[ccn_tokenExpireOn] BIGINT NOT NULL,
|
[token_expire_on] BIGINT NOT NULL,
|
||||||
[ccn_ua] TEXT NOT NULL,
|
[ua] TEXT NOT NULL,
|
||||||
[ccn_ip] TEXT NOT NULL,
|
[ip] TEXT NOT NULL,
|
||||||
|
|
||||||
FOREIGN KEY (ccn_user) REFERENCES user(ccn_name) ON DELETE CASCADE
|
FOREIGN KEY (user) REFERENCES user(name) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE collection(
|
CREATE TABLE collection(
|
||||||
[ccn_uuid] TEXT NOT NULL,
|
[uuid] TEXT NOT NULL,
|
||||||
[ccn_name] TEXT NOT NULL,
|
[name] TEXT NOT NULL,
|
||||||
[ccn_user] TEXT NOT NULL,
|
[user] TEXT NOT NULL,
|
||||||
[ccn_lastChange] TEXT NOT NULL,
|
[last_change] TEXT NOT NULL,
|
||||||
|
|
||||||
PRIMARY KEY (ccn_uuid),
|
PRIMARY KEY (uuid),
|
||||||
FOREIGN KEY (ccn_user) REFERENCES user(ccn_name) ON DELETE CASCADE
|
FOREIGN KEY (user) REFERENCES user(name) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE share(
|
CREATE TABLE share(
|
||||||
[ccn_uuid] TEXT NOT NULL,
|
[uuid] TEXT NOT NULL,
|
||||||
[ccn_target] TEXT NOT NULL,
|
[target] TEXT NOT NULL,
|
||||||
|
|
||||||
FOREIGN KEY (ccn_uuid) REFERENCES collection(ccn_uuid) ON DELETE CASCADE
|
FOREIGN KEY (uuid) REFERENCES collection(uuid) ON DELETE CASCADE
|
||||||
FOREIGN KEY (ccn_target) REFERENCES user(ccn_name) ON DELETE CASCADE
|
FOREIGN KEY (target) REFERENCES user(name) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE calendar(
|
CREATE TABLE calendar(
|
||||||
[ccn_uuid] TEXT NOT NULL,
|
[uuid] TEXT NOT NULL,
|
||||||
[ccn_belongTo] TEXT NOT NULL,
|
[belong_to] TEXT NOT NULL,
|
||||||
|
|
||||||
[ccn_title] TEXT NOT NULL,
|
[title] TEXT NOT NULL,
|
||||||
[ccn_description] TEXT NOT NULL,
|
[description] TEXT NOT NULL,
|
||||||
[ccn_lastChange] TEXT NOT NULL,
|
[last_change] TEXT NOT NULL,
|
||||||
|
|
||||||
[ccn_eventDateTimeStart] BIGINT NOT NULL,
|
[event_date_time_start] BIGINT NOT NULL,
|
||||||
[ccn_eventDateTimeEnd] BIGINT NOT NULL,
|
[event_date_time_end] BIGINT NOT NULL,
|
||||||
[ccn_timezoneOffset] INT NOT NULL,
|
[timezone_offset] INT NOT NULL,
|
||||||
|
|
||||||
[ccn_loopRules] TEXT NOT NULL,
|
[loop_rules] TEXT NOT NULL,
|
||||||
[ccn_loopDateTimeStart] BIGINT NOT NULL,
|
[loop_date_time_start] BIGINT NOT NULL,
|
||||||
[ccn_loopDateTimeEnd] BIGINT NOT NULL,
|
[loop_date_time_end] BIGINT NOT NULL,
|
||||||
|
|
||||||
PRIMARY KEY (ccn_uuid),
|
PRIMARY KEY (uuid),
|
||||||
FOREIGN KEY (ccn_belongTo) REFERENCES collection(ccn_uuid) ON DELETE CASCADE
|
FOREIGN KEY (belong_to) REFERENCES collection(uuid) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE todo(
|
CREATE TABLE todo(
|
||||||
[ccn_uuid] TEXT NOT NULL,
|
[uuid] TEXT NOT NULL,
|
||||||
[ccn_belongTo] TEXT NOT NULL,
|
[belong_to] TEXT NOT NULL,
|
||||||
|
|
||||||
[ccn_data] TEXT NOT NULL,
|
[data] TEXT NOT NULL,
|
||||||
[ccn_lastChange] TEXT NOT NULL,
|
[last_change] TEXT NOT NULL,
|
||||||
|
|
||||||
PRIMARY KEY (ccn_uuid),
|
PRIMARY KEY (uuid),
|
||||||
FOREIGN KEY (ccn_belongTo) REFERENCES user(ccn_name) ON DELETE CASCADE
|
FOREIGN KEY (belong_to) REFERENCES user(name) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
@@ -3,50 +3,62 @@ import random
|
|||||||
import uuid
|
import uuid
|
||||||
import time
|
import time
|
||||||
import math
|
import math
|
||||||
|
import re
|
||||||
|
|
||||||
ValidUsername = set(map(lambda x:chr(x), range(48, 58, 1))) | set(map(lambda x:chr(x), range(65, 91, 1))) | set(map(lambda x:chr(x), range(97, 123, 1)))
|
USERNAME_PATTERN: re.Pattern = re.compile("^[0-9A-Za-z]+$")
|
||||||
ValidPassword = set(map(lambda x:chr(x), range(33, 127, 1)))
|
PASSWORD_PATTERN: re.Pattern = re.compile("^[!-~]+$")
|
||||||
|
|
||||||
def IsValidUsername(strl):
|
|
||||||
return (len(set(strl) - ValidUsername) == 0)
|
|
||||||
|
|
||||||
def IsValidPassword(strl):
|
def IsValidUsername(strl: str) -> bool:
|
||||||
return (len(set(strl) - ValidPassword) == 0)
|
return USERNAME_PATTERN.match(strl) is not None
|
||||||
|
|
||||||
def ComputePasswordHash(password):
|
|
||||||
|
def IsValidPassword(strl: str) -> bool:
|
||||||
|
return PASSWORD_PATTERN.match(strl) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def ComputePasswordHash(password: str) -> str:
|
||||||
s = hashlib.sha256()
|
s = hashlib.sha256()
|
||||||
s.update(password.encode('utf-8'))
|
s.update(password.encode("utf-8"))
|
||||||
return s.hexdigest()
|
return s.hexdigest()
|
||||||
|
|
||||||
def GenerateUUID():
|
|
||||||
|
def GenerateUUID() -> str:
|
||||||
return str(uuid.uuid1())
|
return str(uuid.uuid1())
|
||||||
|
|
||||||
def GenerateToken(username):
|
|
||||||
|
def GenerateToken(username: str) -> str:
|
||||||
s = hashlib.sha256()
|
s = hashlib.sha256()
|
||||||
s.update(username.encode('utf-8'))
|
s.update(username.encode("utf-8"))
|
||||||
s.update(GenerateUUID().encode('utf-8'))
|
s.update(GenerateUUID().encode("utf-8"))
|
||||||
return s.hexdigest()
|
return s.hexdigest()
|
||||||
|
|
||||||
def GenerateSalt():
|
|
||||||
|
def GenerateSalt() -> int:
|
||||||
return random.randint(0, 6172748)
|
return random.randint(0, 6172748)
|
||||||
|
|
||||||
def ComputePasswordHashWithSalt(passwordHashed, salt):
|
|
||||||
|
def ComputePasswordHashWithSalt(passwordHashed: str, salt: int) -> str:
|
||||||
s = hashlib.sha256()
|
s = hashlib.sha256()
|
||||||
s.update((passwordHashed + str(salt)).encode('utf-8'))
|
s.update((passwordHashed + str(salt)).encode("utf-8"))
|
||||||
return s.hexdigest()
|
return s.hexdigest()
|
||||||
|
|
||||||
def GetCurrentTimestamp():
|
|
||||||
|
def GetCurrentTimestamp() -> int:
|
||||||
return int(time.time())
|
return int(time.time())
|
||||||
|
|
||||||
def GetTokenExpireOn():
|
|
||||||
return GetCurrentTimestamp() + 60 * 60 * 24 * 2 # add 2 day from now
|
|
||||||
|
|
||||||
def Str2Bool(strl):
|
def GetTokenExpireOn() -> int:
|
||||||
return strl.lower() == 'true'
|
return GetCurrentTimestamp() + 60 * 60 * 24 * 2 # add 2 day from now
|
||||||
|
|
||||||
def GCD(a, b):
|
|
||||||
|
def Str2Bool(strl: str) -> bool:
|
||||||
|
return strl.lower() == "true"
|
||||||
|
|
||||||
|
|
||||||
|
def GCD(a: int, b: int) -> int:
|
||||||
return math.gcd(a, b)
|
return math.gcd(a, b)
|
||||||
|
|
||||||
def LCM(a, b):
|
|
||||||
return int(a * b / GCD(a, b))
|
def LCM(a: int, b: int) -> int:
|
||||||
|
return (a * b) // GCD(a, b)
|
||||||
|
|||||||
BIN
frontend-legacy/static/image/icon.png
Normal file
BIN
frontend-legacy/static/image/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
@@ -14,10 +14,13 @@
|
|||||||
"lint:eslint": "eslint . --fix --cache"
|
"lint:eslint": "eslint . --fix --cache"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^7.2.0",
|
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^7.2.0",
|
||||||
"@fortawesome/vue-fontawesome": "^3.2.0",
|
"@fortawesome/vue-fontawesome": "^3.2.0",
|
||||||
"bulma": "^1.0.4",
|
"axios": "1.14.0",
|
||||||
|
"bulma": "0.9.1",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
|
"pinia-plugin-persistedstate": "^4.7.1",
|
||||||
"vue": "^3.5.32",
|
"vue": "^3.5.32",
|
||||||
"vue-router": "^5.0.4"
|
"vue-router": "^5.0.4"
|
||||||
},
|
},
|
||||||
|
|||||||
480
frontend/pnpm-lock.yaml
generated
480
frontend/pnpm-lock.yaml
generated
@@ -8,18 +8,27 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@fortawesome/fontawesome-free':
|
'@fortawesome/fontawesome-svg-core':
|
||||||
|
specifier: ^7.2.0
|
||||||
|
version: 7.2.0
|
||||||
|
'@fortawesome/free-solid-svg-icons':
|
||||||
specifier: ^7.2.0
|
specifier: ^7.2.0
|
||||||
version: 7.2.0
|
version: 7.2.0
|
||||||
'@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: ^1.0.4
|
specifier: 0.9.1
|
||||||
version: 1.0.4
|
version: 0.9.1
|
||||||
pinia:
|
pinia:
|
||||||
specifier: ^3.0.4
|
specifier: ^3.0.4
|
||||||
version: 3.0.4(typescript@6.0.3)(vue@3.5.33(typescript@6.0.3))
|
version: 3.0.4(typescript@6.0.3)(vue@3.5.33(typescript@6.0.3))
|
||||||
|
pinia-plugin-persistedstate:
|
||||||
|
specifier: ^4.7.1
|
||||||
|
version: 4.7.1(pinia@3.0.4(typescript@6.0.3)(vue@3.5.33(typescript@6.0.3)))
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.5.32
|
specifier: ^3.5.32
|
||||||
version: 3.5.33(typescript@6.0.3)
|
version: 3.5.33(typescript@6.0.3)
|
||||||
@@ -35,7 +44,7 @@ importers:
|
|||||||
version: 24.12.2
|
version: 24.12.2
|
||||||
'@vitejs/plugin-vue':
|
'@vitejs/plugin-vue':
|
||||||
specifier: ^6.0.6
|
specifier: ^6.0.6
|
||||||
version: 6.0.6(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3))(vue@3.5.33(typescript@6.0.3))
|
version: 6.0.6(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.3))(vue@3.5.33(typescript@6.0.3))
|
||||||
'@vue/eslint-config-typescript':
|
'@vue/eslint-config-typescript':
|
||||||
specifier: ^14.7.0
|
specifier: ^14.7.0
|
||||||
version: 14.7.0(eslint-plugin-vue@10.8.0(@typescript-eslint/parser@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.2.1(jiti@2.6.1))))(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
|
version: 14.7.0(eslint-plugin-vue@10.8.0(@typescript-eslint/parser@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.2.1(jiti@2.6.1))))(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
|
||||||
@@ -65,10 +74,10 @@ importers:
|
|||||||
version: 6.0.3
|
version: 6.0.3
|
||||||
vite:
|
vite:
|
||||||
specifier: ^8.0.8
|
specifier: ^8.0.8
|
||||||
version: 8.0.10(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3)
|
version: 8.0.10(@types/node@24.12.2)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.3)
|
||||||
vite-plugin-vue-devtools:
|
vite-plugin-vue-devtools:
|
||||||
specifier: ^8.1.1
|
specifier: ^8.1.1
|
||||||
version: 8.1.1(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3))(vue@3.5.33(typescript@6.0.3))
|
version: 8.1.1(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.3))(vue@3.5.33(typescript@6.0.3))
|
||||||
vue-tsc:
|
vue-tsc:
|
||||||
specifier: ^3.2.6
|
specifier: ^3.2.6
|
||||||
version: 3.2.7(typescript@6.0.3)
|
version: 3.2.7(typescript@6.0.3)
|
||||||
@@ -258,14 +267,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-IpR0bER9FY25p+e7BmFH25MZKEwFHTfRAfhOyJubgiDnoJNsSvJ7nigLraHtp4VOG/cy8D7uiV0dLkHOne5Fhw==}
|
resolution: {integrity: sha512-IpR0bER9FY25p+e7BmFH25MZKEwFHTfRAfhOyJubgiDnoJNsSvJ7nigLraHtp4VOG/cy8D7uiV0dLkHOne5Fhw==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
'@fortawesome/fontawesome-free@7.2.0':
|
|
||||||
resolution: {integrity: sha512-3DguDv/oUE+7vjMeTSOjCSG+KeawgVQOHrKRnvUuqYh1mfArrh7s+s8hXW3e4RerBA1+Wh+hBqf8sJNpqNrBWg==}
|
|
||||||
engines: {node: '>=6'}
|
|
||||||
|
|
||||||
'@fortawesome/fontawesome-svg-core@7.2.0':
|
'@fortawesome/fontawesome-svg-core@7.2.0':
|
||||||
resolution: {integrity: sha512-6639htZMjEkwskf3J+e6/iar+4cTNM9qhoWuRfj9F3eJD6r7iCzV1SWnQr2Mdv0QT0suuqU8BoJCZUyCtP9R4Q==}
|
resolution: {integrity: sha512-6639htZMjEkwskf3J+e6/iar+4cTNM9qhoWuRfj9F3eJD6r7iCzV1SWnQr2Mdv0QT0suuqU8BoJCZUyCtP9R4Q==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
'@fortawesome/free-solid-svg-icons@7.2.0':
|
||||||
|
resolution: {integrity: sha512-YTVITFGN0/24PxzXrwqCgnyd7njDuzp5ZvaCx5nq/jg55kUYd94Nj8UTchBdBofi/L0nwRfjGOg0E41d2u9T1w==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
'@fortawesome/vue-fontawesome@3.2.0':
|
'@fortawesome/vue-fontawesome@3.2.0':
|
||||||
resolution: {integrity: sha512-7BwGjTZn8QDvVEIu8fvkHhsDRRv//tq7jtsldaDhF3dE1fyWLIQcEg3zvIzy33su7kcppWsZZ6XRYP5wp3UCgQ==}
|
resolution: {integrity: sha512-7BwGjTZn8QDvVEIu8fvkHhsDRRv//tq7jtsldaDhF3dE1fyWLIQcEg3zvIzy33su7kcppWsZZ6XRYP5wp3UCgQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -451,6 +460,94 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@parcel/watcher-android-arm64@2.5.6':
|
||||||
|
resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@parcel/watcher-darwin-arm64@2.5.6':
|
||||||
|
resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@parcel/watcher-darwin-x64@2.5.6':
|
||||||
|
resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@parcel/watcher-freebsd-x64@2.5.6':
|
||||||
|
resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm-glibc@2.5.6':
|
||||||
|
resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm-musl@2.5.6':
|
||||||
|
resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm64-glibc@2.5.6':
|
||||||
|
resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm64-musl@2.5.6':
|
||||||
|
resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-x64-glibc@2.5.6':
|
||||||
|
resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-x64-musl@2.5.6':
|
||||||
|
resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@parcel/watcher-win32-arm64@2.5.6':
|
||||||
|
resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@parcel/watcher-win32-ia32@2.5.6':
|
||||||
|
resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@parcel/watcher-win32-x64@2.5.6':
|
||||||
|
resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@parcel/watcher@2.5.6':
|
||||||
|
resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
|
||||||
'@polka/url@1.0.0-next.29':
|
'@polka/url@1.0.0-next.29':
|
||||||
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
||||||
|
|
||||||
@@ -782,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}
|
||||||
@@ -810,20 +913,32 @@ packages:
|
|||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
bulma@1.0.4:
|
bulma@0.9.1:
|
||||||
resolution: {integrity: sha512-Ffb6YGXDiZYX3cqvSbHWqQ8+LkX6tVoTcZuVB3lm93sbAVXlO0D6QlOTMnV6g18gILpAXqkG2z9hf9z4hCjz2g==}
|
resolution: {integrity: sha512-LSF69OumXg2HSKl2+rN0/OEXJy7WFEb681wtBlNS/ulJYR27J3rORHibdXZ6GVb/vyUzzYK/Arjyh56wjbFedA==}
|
||||||
|
|
||||||
bundle-name@4.1.0:
|
bundle-name@4.1.0:
|
||||||
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==}
|
||||||
|
|
||||||
|
chokidar@4.0.3:
|
||||||
|
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||||
|
engines: {node: '>= 14.16.0'}
|
||||||
|
|
||||||
chokidar@5.0.0:
|
chokidar@5.0.0:
|
||||||
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==}
|
||||||
|
|
||||||
@@ -873,10 +988,21 @@ packages:
|
|||||||
resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==}
|
resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
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:
|
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==}
|
||||||
|
|
||||||
@@ -887,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'}
|
||||||
@@ -1006,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'}
|
||||||
@@ -1023,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==}
|
||||||
|
|
||||||
@@ -1034,6 +1216,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
|
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
|
|
||||||
|
immutable@5.1.5:
|
||||||
|
resolution: {integrity: sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==}
|
||||||
|
|
||||||
imurmurhash@0.1.4:
|
imurmurhash@0.1.4:
|
||||||
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
|
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
|
||||||
engines: {node: '>=0.8.19'}
|
engines: {node: '>=0.8.19'}
|
||||||
@@ -1210,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'}
|
||||||
@@ -1222,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}
|
||||||
@@ -1250,6 +1447,9 @@ packages:
|
|||||||
natural-compare@1.4.0:
|
natural-compare@1.4.0:
|
||||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||||
|
|
||||||
|
node-addon-api@7.1.1:
|
||||||
|
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
||||||
|
|
||||||
node-releases@2.0.38:
|
node-releases@2.0.38:
|
||||||
resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==}
|
resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==}
|
||||||
|
|
||||||
@@ -1330,6 +1530,20 @@ packages:
|
|||||||
engines: {node: '>=0.10'}
|
engines: {node: '>=0.10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
pinia-plugin-persistedstate@4.7.1:
|
||||||
|
resolution: {integrity: sha512-WHOqh2esDlR3eAaknPbqXrkkj0D24h8shrDPqysgCFR6ghqP/fpFfJmMPJp0gETHsvrh9YNNg6dQfo2OEtDnIQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@nuxt/kit': '>=3.0.0'
|
||||||
|
'@pinia/nuxt': '>=0.10.0'
|
||||||
|
pinia: '>=3.0.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@nuxt/kit':
|
||||||
|
optional: true
|
||||||
|
'@pinia/nuxt':
|
||||||
|
optional: true
|
||||||
|
pinia:
|
||||||
|
optional: true
|
||||||
|
|
||||||
pinia@3.0.4:
|
pinia@3.0.4:
|
||||||
resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==}
|
resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1357,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'}
|
||||||
@@ -1371,6 +1589,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==}
|
resolution: {integrity: sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==}
|
||||||
engines: {node: ^18.17.0 || >=20.5.0}
|
engines: {node: ^18.17.0 || >=20.5.0}
|
||||||
|
|
||||||
|
readdirp@4.1.2:
|
||||||
|
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||||
|
engines: {node: '>= 14.18.0'}
|
||||||
|
|
||||||
readdirp@5.0.0:
|
readdirp@5.0.0:
|
||||||
resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
|
resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
|
||||||
engines: {node: '>= 20.19.0'}
|
engines: {node: '>= 20.19.0'}
|
||||||
@@ -1394,6 +1616,11 @@ packages:
|
|||||||
run-parallel@1.2.0:
|
run-parallel@1.2.0:
|
||||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||||
|
|
||||||
|
sass@1.99.0:
|
||||||
|
resolution: {integrity: sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
scule@1.3.0:
|
scule@1.3.0:
|
||||||
resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==}
|
resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==}
|
||||||
|
|
||||||
@@ -1887,12 +2114,14 @@ snapshots:
|
|||||||
|
|
||||||
'@fortawesome/fontawesome-common-types@7.2.0': {}
|
'@fortawesome/fontawesome-common-types@7.2.0': {}
|
||||||
|
|
||||||
'@fortawesome/fontawesome-free@7.2.0': {}
|
|
||||||
|
|
||||||
'@fortawesome/fontawesome-svg-core@7.2.0':
|
'@fortawesome/fontawesome-svg-core@7.2.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@fortawesome/fontawesome-common-types': 7.2.0
|
'@fortawesome/fontawesome-common-types': 7.2.0
|
||||||
|
|
||||||
|
'@fortawesome/free-solid-svg-icons@7.2.0':
|
||||||
|
dependencies:
|
||||||
|
'@fortawesome/fontawesome-common-types': 7.2.0
|
||||||
|
|
||||||
'@fortawesome/vue-fontawesome@3.2.0(@fortawesome/fontawesome-svg-core@7.2.0)(vue@3.5.33(typescript@6.0.3))':
|
'@fortawesome/vue-fontawesome@3.2.0(@fortawesome/fontawesome-svg-core@7.2.0)(vue@3.5.33(typescript@6.0.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@fortawesome/fontawesome-svg-core': 7.2.0
|
'@fortawesome/fontawesome-svg-core': 7.2.0
|
||||||
@@ -2011,6 +2240,67 @@ snapshots:
|
|||||||
'@oxlint/binding-win32-x64-msvc@1.60.0':
|
'@oxlint/binding-win32-x64-msvc@1.60.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-android-arm64@2.5.6':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-darwin-arm64@2.5.6':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-darwin-x64@2.5.6':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-freebsd-x64@2.5.6':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm-glibc@2.5.6':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm-musl@2.5.6':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm64-glibc@2.5.6':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm64-musl@2.5.6':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-x64-glibc@2.5.6':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-x64-musl@2.5.6':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-win32-arm64@2.5.6':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-win32-ia32@2.5.6':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-win32-x64@2.5.6':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher@2.5.6':
|
||||||
|
dependencies:
|
||||||
|
detect-libc: 2.1.2
|
||||||
|
is-glob: 4.0.3
|
||||||
|
node-addon-api: 7.1.1
|
||||||
|
picomatch: 4.0.4
|
||||||
|
optionalDependencies:
|
||||||
|
'@parcel/watcher-android-arm64': 2.5.6
|
||||||
|
'@parcel/watcher-darwin-arm64': 2.5.6
|
||||||
|
'@parcel/watcher-darwin-x64': 2.5.6
|
||||||
|
'@parcel/watcher-freebsd-x64': 2.5.6
|
||||||
|
'@parcel/watcher-linux-arm-glibc': 2.5.6
|
||||||
|
'@parcel/watcher-linux-arm-musl': 2.5.6
|
||||||
|
'@parcel/watcher-linux-arm64-glibc': 2.5.6
|
||||||
|
'@parcel/watcher-linux-arm64-musl': 2.5.6
|
||||||
|
'@parcel/watcher-linux-x64-glibc': 2.5.6
|
||||||
|
'@parcel/watcher-linux-x64-musl': 2.5.6
|
||||||
|
'@parcel/watcher-win32-arm64': 2.5.6
|
||||||
|
'@parcel/watcher-win32-ia32': 2.5.6
|
||||||
|
'@parcel/watcher-win32-x64': 2.5.6
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@polka/url@1.0.0-next.29': {}
|
'@polka/url@1.0.0-next.29': {}
|
||||||
|
|
||||||
'@rolldown/binding-android-arm64@1.0.0-rc.17':
|
'@rolldown/binding-android-arm64@1.0.0-rc.17':
|
||||||
@@ -2174,10 +2464,10 @@ snapshots:
|
|||||||
'@typescript-eslint/types': 8.59.1
|
'@typescript-eslint/types': 8.59.1
|
||||||
eslint-visitor-keys: 5.0.1
|
eslint-visitor-keys: 5.0.1
|
||||||
|
|
||||||
'@vitejs/plugin-vue@6.0.6(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3))(vue@3.5.33(typescript@6.0.3))':
|
'@vitejs/plugin-vue@6.0.6(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.3))(vue@3.5.33(typescript@6.0.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rolldown/pluginutils': 1.0.0-rc.13
|
'@rolldown/pluginutils': 1.0.0-rc.13
|
||||||
vite: 8.0.10(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3)
|
vite: 8.0.10(@types/node@24.12.2)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.3)
|
||||||
vue: 3.5.33(typescript@6.0.3)
|
vue: 3.5.33(typescript@6.0.3)
|
||||||
|
|
||||||
'@volar/language-core@2.4.28':
|
'@volar/language-core@2.4.28':
|
||||||
@@ -2379,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: {}
|
||||||
@@ -2403,18 +2703,32 @@ snapshots:
|
|||||||
node-releases: 2.0.38
|
node-releases: 2.0.38
|
||||||
update-browserslist-db: 1.2.3(browserslist@4.28.2)
|
update-browserslist-db: 1.2.3(browserslist@4.28.2)
|
||||||
|
|
||||||
bulma@1.0.4: {}
|
bulma@0.9.1: {}
|
||||||
|
|
||||||
bundle-name@4.1.0:
|
bundle-name@4.1.0:
|
||||||
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:
|
||||||
|
dependencies:
|
||||||
|
readdirp: 4.1.2
|
||||||
|
optional: true
|
||||||
|
|
||||||
chokidar@5.0.0:
|
chokidar@5.0.0:
|
||||||
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: {}
|
||||||
@@ -2450,14 +2764,39 @@ snapshots:
|
|||||||
|
|
||||||
define-lazy-prop@3.0.0: {}
|
define-lazy-prop@3.0.0: {}
|
||||||
|
|
||||||
|
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: {}
|
||||||
@@ -2592,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
|
||||||
@@ -2605,12 +2974,27 @@ 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: {}
|
||||||
|
|
||||||
ignore@7.0.5: {}
|
ignore@7.0.5: {}
|
||||||
|
|
||||||
|
immutable@5.1.5:
|
||||||
|
optional: true
|
||||||
|
|
||||||
imurmurhash@0.1.4: {}
|
imurmurhash@0.1.4: {}
|
||||||
|
|
||||||
is-docker@3.0.0: {}
|
is-docker@3.0.0: {}
|
||||||
@@ -2737,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: {}
|
||||||
@@ -2746,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
|
||||||
@@ -2769,6 +3161,9 @@ snapshots:
|
|||||||
|
|
||||||
natural-compare@1.4.0: {}
|
natural-compare@1.4.0: {}
|
||||||
|
|
||||||
|
node-addon-api@7.1.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
node-releases@2.0.38: {}
|
node-releases@2.0.38: {}
|
||||||
|
|
||||||
npm-normalize-package-bin@4.0.0: {}
|
npm-normalize-package-bin@4.0.0: {}
|
||||||
@@ -2856,6 +3251,12 @@ snapshots:
|
|||||||
|
|
||||||
pidtree@0.6.0: {}
|
pidtree@0.6.0: {}
|
||||||
|
|
||||||
|
pinia-plugin-persistedstate@4.7.1(pinia@3.0.4(typescript@6.0.3)(vue@3.5.33(typescript@6.0.3))):
|
||||||
|
dependencies:
|
||||||
|
defu: 6.1.7
|
||||||
|
optionalDependencies:
|
||||||
|
pinia: 3.0.4(typescript@6.0.3)(vue@3.5.33(typescript@6.0.3))
|
||||||
|
|
||||||
pinia@3.0.4(typescript@6.0.3)(vue@3.5.33(typescript@6.0.3)):
|
pinia@3.0.4(typescript@6.0.3)(vue@3.5.33(typescript@6.0.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/devtools-api': 7.7.9
|
'@vue/devtools-api': 7.7.9
|
||||||
@@ -2888,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: {}
|
||||||
@@ -2899,6 +3302,9 @@ snapshots:
|
|||||||
json-parse-even-better-errors: 4.0.0
|
json-parse-even-better-errors: 4.0.0
|
||||||
npm-normalize-package-bin: 4.0.0
|
npm-normalize-package-bin: 4.0.0
|
||||||
|
|
||||||
|
readdirp@4.1.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
readdirp@5.0.0: {}
|
readdirp@5.0.0: {}
|
||||||
|
|
||||||
reusify@1.1.0: {}
|
reusify@1.1.0: {}
|
||||||
@@ -2932,6 +3338,15 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
queue-microtask: 1.2.3
|
queue-microtask: 1.2.3
|
||||||
|
|
||||||
|
sass@1.99.0:
|
||||||
|
dependencies:
|
||||||
|
chokidar: 4.0.3
|
||||||
|
immutable: 5.1.5
|
||||||
|
source-map-js: 1.2.1
|
||||||
|
optionalDependencies:
|
||||||
|
'@parcel/watcher': 2.5.6
|
||||||
|
optional: true
|
||||||
|
|
||||||
scule@1.3.0: {}
|
scule@1.3.0: {}
|
||||||
|
|
||||||
semver@6.3.1: {}
|
semver@6.3.1: {}
|
||||||
@@ -3022,17 +3437,17 @@ snapshots:
|
|||||||
|
|
||||||
util-deprecate@1.0.2: {}
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
vite-dev-rpc@1.1.0(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3)):
|
vite-dev-rpc@1.1.0(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
birpc: 2.9.0
|
birpc: 2.9.0
|
||||||
vite: 8.0.10(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3)
|
vite: 8.0.10(@types/node@24.12.2)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.3)
|
||||||
vite-hot-client: 2.1.0(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3))
|
vite-hot-client: 2.1.0(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.3))
|
||||||
|
|
||||||
vite-hot-client@2.1.0(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3)):
|
vite-hot-client@2.1.0(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
vite: 8.0.10(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3)
|
vite: 8.0.10(@types/node@24.12.2)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.3)
|
||||||
|
|
||||||
vite-plugin-inspect@11.3.3(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3)):
|
vite-plugin-inspect@11.3.3(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
ansis: 4.2.0
|
ansis: 4.2.0
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
@@ -3042,26 +3457,26 @@ snapshots:
|
|||||||
perfect-debounce: 2.1.0
|
perfect-debounce: 2.1.0
|
||||||
sirv: 3.0.2
|
sirv: 3.0.2
|
||||||
unplugin-utils: 0.3.1
|
unplugin-utils: 0.3.1
|
||||||
vite: 8.0.10(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3)
|
vite: 8.0.10(@types/node@24.12.2)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.3)
|
||||||
vite-dev-rpc: 1.1.0(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3))
|
vite-dev-rpc: 1.1.0(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.3))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
vite-plugin-vue-devtools@8.1.1(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3))(vue@3.5.33(typescript@6.0.3)):
|
vite-plugin-vue-devtools@8.1.1(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.3))(vue@3.5.33(typescript@6.0.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/devtools-core': 8.1.1(vue@3.5.33(typescript@6.0.3))
|
'@vue/devtools-core': 8.1.1(vue@3.5.33(typescript@6.0.3))
|
||||||
'@vue/devtools-kit': 8.1.1
|
'@vue/devtools-kit': 8.1.1
|
||||||
'@vue/devtools-shared': 8.1.1
|
'@vue/devtools-shared': 8.1.1
|
||||||
sirv: 3.0.2
|
sirv: 3.0.2
|
||||||
vite: 8.0.10(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3)
|
vite: 8.0.10(@types/node@24.12.2)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.3)
|
||||||
vite-plugin-inspect: 11.3.3(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3))
|
vite-plugin-inspect: 11.3.3(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.3))
|
||||||
vite-plugin-vue-inspector: 5.4.0(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3))
|
vite-plugin-vue-inspector: 5.4.0(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.3))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@nuxt/kit'
|
- '@nuxt/kit'
|
||||||
- supports-color
|
- supports-color
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
vite-plugin-vue-inspector@5.4.0(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3)):
|
vite-plugin-vue-inspector@5.4.0(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.29.0
|
||||||
'@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0)
|
'@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0)
|
||||||
@@ -3072,11 +3487,11 @@ snapshots:
|
|||||||
'@vue/compiler-dom': 3.5.33
|
'@vue/compiler-dom': 3.5.33
|
||||||
kolorist: 1.8.0
|
kolorist: 1.8.0
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
vite: 8.0.10(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3)
|
vite: 8.0.10(@types/node@24.12.2)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.3)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3):
|
vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
lightningcss: 1.32.0
|
lightningcss: 1.32.0
|
||||||
picomatch: 4.0.4
|
picomatch: 4.0.4
|
||||||
@@ -3087,6 +3502,7 @@ snapshots:
|
|||||||
'@types/node': 24.12.2
|
'@types/node': 24.12.2
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
jiti: 2.6.1
|
jiti: 2.6.1
|
||||||
|
sass: 1.99.0
|
||||||
yaml: 2.8.3
|
yaml: 2.8.3
|
||||||
|
|
||||||
vscode-uri@3.1.0: {}
|
vscode-uri@3.1.0: {}
|
||||||
|
|||||||
3
frontend/public/index.scss
Normal file
3
frontend/public/index.scss
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// 导入 Bulma
|
||||||
|
@charset "utf-8";
|
||||||
|
@import "bulma/bulma.sass";
|
||||||
@@ -1,49 +1,92 @@
|
|||||||
<script setup lang="ts"></script>
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useLanguageStore } from './stores/language';
|
||||||
|
import { useTokenStore } from './stores/token';
|
||||||
|
import MessageBox from '@/components/MessageBox.vue';
|
||||||
|
import { logout as apiCommonLogout } from './api/common';
|
||||||
|
import { goToHome } from '@/router';
|
||||||
|
|
||||||
|
const language = useLanguageStore();
|
||||||
|
const token = useTokenStore();
|
||||||
|
|
||||||
|
const isBurgerActive = ref<boolean>(false);
|
||||||
|
|
||||||
|
const messagebox = ref<InstanceType<typeof MessageBox> | null>(null);
|
||||||
|
|
||||||
|
const logout = async () => {
|
||||||
|
const tokenStore = useTokenStore();
|
||||||
|
const rv = await apiCommonLogout(tokenStore.currentToken);
|
||||||
|
if (rv) {
|
||||||
|
// OK. We logged out.
|
||||||
|
// Clear token.
|
||||||
|
tokenStore.logout();
|
||||||
|
// And go to Home page
|
||||||
|
goToHome();
|
||||||
|
} else {
|
||||||
|
// Show logout error.
|
||||||
|
messagebox.value?.show("Fail to logout due to unknow reason. Consider refreshing page to solve problem.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process burger menu.
|
||||||
|
// This is copied from Bulma website and modified for Vue.
|
||||||
|
// Check for click events on the navbar burger icon
|
||||||
|
const toggleBurger = () => {
|
||||||
|
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
|
||||||
|
isBurgerActive.value = !isBurgerActive
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<nav class="navbar has-shadow is-spaced bd-navbar" role="navigation" aria-label="main navigation">
|
<nav class="navbar has-shadow is-spaced bd-navbar" role="navigation" aria-label="main navigation">
|
||||||
<div class="navbar-brand">
|
<div class="navbar-brand">
|
||||||
<router-link class="navbar-item" to="/">
|
<router-link class="navbar-item" to="/">
|
||||||
<img src="/public/favicon.ico"><b style="margin:0 0 0 14px;">coconut-leaf</b>
|
<img src="/public/favicon.ico"><b style="margin:0 0 0 14px;">coconut-leaf</b>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<a role="button" class="navbar-burger burger" aria-label="menu" aria-expanded="false"
|
<a role="button" class="navbar-burger burger" :class="{ 'is-active': isBurgerActive }" @click="toggleBurger"
|
||||||
data-target="coleaf-navbar">
|
aria-label="menu" aria-expanded="false" data-target="coleaf-navbar">
|
||||||
<span aria-hidden="true"></span>
|
<span aria-hidden="true"></span>
|
||||||
<span aria-hidden="true"></span>
|
<span aria-hidden="true"></span>
|
||||||
<span aria-hidden="true"></span>
|
<span aria-hidden="true"></span>
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="coleaf-navbar" class="navbar-menu" :class="{ 'is-active': isBurgerActive }">
|
||||||
|
<div class="navbar-start">
|
||||||
|
<router-link class="navbar-item" to="/home">Home</router-link>
|
||||||
|
<router-link v-if="token.isLoggedIn" class="navbar-item" to="/collection">Collection</router-link>
|
||||||
|
<router-link v-if="token.isLoggedIn" class="navbar-item" to="/calendar">Calendar</router-link>
|
||||||
|
<router-link v-if="token.isLoggedIn" class="navbar-item" to="/todo">Todo</router-link>
|
||||||
|
<router-link v-if="token.isLoggedIn" class="navbar-item" to="/admin">Admin</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="coleaf-navbar" class="navbar-menu">
|
<div class="navbar-end">
|
||||||
<div class="navbar-start">
|
<p class="navbar-item">
|
||||||
<router-link class="navbar-item" to="/home">Home</router-link>
|
<router-link v-if="!token.isLoggedIn" class="button is-primary" to="/login">Login</router-link>
|
||||||
<router-link class="navbar-item" to="/collection">Collection</router-link>
|
</p>
|
||||||
<router-link class="navbar-item" to="/calendar">Calendar</router-link>
|
<p class="navbar-item">
|
||||||
<router-link class="navbar-item" to="/todo">Todo</router-link>
|
<a v-if="token.isLoggedIn" class="button is-primary" @click="logout">Logout</a>
|
||||||
<router-link class="navbar-item" to="/admin">Admin</router-link>
|
</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="navbar-end">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
<p class="navbar-item">
|
<a v-if="language.isEnglish" class="navbar-link">English</a>
|
||||||
<a class="button is-primary" href="/login">Login</a>
|
<a v-else-if="language.isSimplifiedChinese" class="navbar-link">简体中文</a>
|
||||||
</p>
|
|
||||||
<p class="navbar-item">
|
|
||||||
<a class="button is-primary">Logout</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-dropdown">
|
||||||
<a class="navbar-link"></a>
|
<a @click="language.changeToEnglish()" class="navbar-item">English</a>
|
||||||
<div class="navbar-dropdown">
|
<a @click="language.changeToSimplifiedChinese()" class="navbar-item">简体中文</a>
|
||||||
<a language="en-US" class="navbar-item">English</a>
|
|
||||||
<a language="zh-CN" class="navbar-item">简体中文</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</div>
|
||||||
<!-- The output result of router -->
|
</nav>
|
||||||
<router-view></router-view>
|
|
||||||
|
<!-- The output result of router -->
|
||||||
|
<router-view></router-view>
|
||||||
|
|
||||||
|
<MessageBox ref="messagebox" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
75
frontend/src/api/admin.ts
Normal file
75
frontend/src/api/admin.ts
Normal 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 });
|
||||||
|
}
|
||||||
141
frontend/src/api/calendar.ts
Normal file
141
frontend/src/api/calendar.ts
Normal 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 });
|
||||||
|
}
|
||||||
126
frontend/src/api/collection.ts
Normal file
126
frontend/src/api/collection.ts
Normal 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 });
|
||||||
|
}
|
||||||
43
frontend/src/api/common.ts
Normal file
43
frontend/src/api/common.ts
Normal 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
57
frontend/src/api/index.ts
Normal 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 只有在网络故障时才会 reject,HTTP 404/500 不会)
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`HTTP failed: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 JSON body
|
||||||
|
// 注意:response.json() 返回的是一个 Promise,所以需要 await
|
||||||
|
const payload = await response.json() as ApiResponse<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;
|
||||||
|
}
|
||||||
|
|
||||||
45
frontend/src/api/profile.ts
Normal file
45
frontend/src/api/profile.ts
Normal 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
56
frontend/src/api/todo.ts
Normal 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 });
|
||||||
|
}
|
||||||
41
frontend/src/components/MessageBox.vue
Normal file
41
frontend/src/components/MessageBox.vue
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const isVisible = ref(false);
|
||||||
|
const title = ref<string>("");
|
||||||
|
const content = ref<string>("");
|
||||||
|
|
||||||
|
const show = (_content: string, _title?: string) => {
|
||||||
|
title.value = _title ?? "Notification";
|
||||||
|
content.value = _content;
|
||||||
|
isVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hide = () => {
|
||||||
|
isVisible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
show
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="modal" :class="{ 'is-active': isVisible }" style="float: left; position: fixed; top: 0; bottom: 0; left: 0; right: 0;">
|
||||||
|
<div class="modal-background"></div>
|
||||||
|
<div class="modal-card">
|
||||||
|
<header class="modal-card-head">
|
||||||
|
<p class="modal-card-title">{{ title }}</p>
|
||||||
|
<button class="delete" aria-label="close" @click="hide"></button>
|
||||||
|
</header>
|
||||||
|
<div class="modal-card-body">
|
||||||
|
<p>{{ content }}</p>
|
||||||
|
</div>
|
||||||
|
<footer class="modal-card-foot">
|
||||||
|
<button class="button is-success" @click="hide">OK</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -1,12 +1,25 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
|
import { faUser, faLock } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||||
|
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
|
||||||
const app = createApp(App)
|
import '../public/index.scss'
|
||||||
|
|
||||||
app.use(createPinia())
|
const pinia = createPinia();
|
||||||
app.use(router)
|
pinia.use(piniaPluginPersistedstate);
|
||||||
|
|
||||||
app.mount('#app')
|
const app = createApp(App);
|
||||||
|
app.use(pinia);
|
||||||
|
app.use(router);
|
||||||
|
|
||||||
|
library.add(faUser, faLock);
|
||||||
|
app.component('font-awesome-icon', FontAwesomeIcon);
|
||||||
|
|
||||||
|
app.mount('#app');
|
||||||
|
|||||||
@@ -1,23 +1,29 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import { useTokenStore } from '@/stores/token'
|
||||||
|
|
||||||
import Home from '../views/Home.vue'
|
import Home from '@/views/Home.vue'
|
||||||
import Collection from '@/views/Collection.vue'
|
import Collection from '@/views/Collection.vue'
|
||||||
import Calendar from '@/views/Calendar.vue'
|
import Calendar from '@/views/Calendar.vue'
|
||||||
|
import CalendarEvent from '@/views/CalendarEvent.vue'
|
||||||
import Todo from '@/views/Todo.vue'
|
import Todo from '@/views/Todo.vue'
|
||||||
import Admin from '@/views/Admin.vue'
|
import Admin from '@/views/Admin.vue'
|
||||||
|
import Login from '@/views/Login.vue'
|
||||||
|
|
||||||
import Page404 from '@/views/Page404.vue'
|
import NotFound from '@/views/NotFound.vue'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{ path: '/home', component: Home },
|
{ path: '/home', name: "Home", component: Home },
|
||||||
{ path: '/collection', component: Collection },
|
{ path: '/collection', name: "Collection", meta: { requireLoggedInCheck: true }, component: Collection },
|
||||||
{ path: '/calendar', component: Calendar},
|
{ path: '/calendar', name: "Calendar", meta: { requireLoggedInCheck: true }, component: Calendar },
|
||||||
{ path: '/todo', component: Todo},
|
{ path: '/todo', name: "Todo", meta: { requireLoggedInCheck: true }, component: Todo },
|
||||||
{ path: '/admin', component: Admin },
|
{ path: '/admin', name: "Admin", meta: { requireLoggedInCheck: true }, component: Admin },
|
||||||
|
|
||||||
{ path: '/404', component: Page404 },
|
{ path: '/calendar/event', name: "CalendarEvent", meta: { requireLoggedInCheck: true }, component: CalendarEvent },
|
||||||
|
{ path: '/login', name: "Collection", meta: { requireLoggedOutCheck: true }, component: Login },
|
||||||
|
|
||||||
{ path: '/', redirect: '/home' },
|
{ path: '/404', name: "NotFound", component: NotFound },
|
||||||
|
|
||||||
|
{ path: '/', name: "Default", redirect: '/home' },
|
||||||
{ path: '/:pathMatch(.*)*', redirect: '/404' },
|
{ path: '/:pathMatch(.*)*', redirect: '/404' },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -26,4 +32,24 @@ const router = createRouter({
|
|||||||
routes: routes,
|
routes: routes,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.beforeEach((to, from) => {
|
||||||
|
// Only check for those flagged.
|
||||||
|
const token = useTokenStore();
|
||||||
|
if (to.meta.requireLoggedInCheck) {
|
||||||
|
if (!token.isLoggedIn) {
|
||||||
|
return { name: 'Default', replace: true };
|
||||||
|
}
|
||||||
|
} else if (to.meta.requireLoggedOutCheck) {
|
||||||
|
if (token.isLoggedIn) {
|
||||||
|
return { name: 'Default', replace: true };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const goToHome = () => {
|
||||||
|
router.push({ name: 'Home' })
|
||||||
|
}
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
import { ref, computed } from 'vue'
|
|
||||||
import { defineStore } from 'pinia'
|
|
||||||
|
|
||||||
export const useCounterStore = defineStore('counter', () => {
|
|
||||||
const count = ref(0)
|
|
||||||
const doubleCount = computed(() => count.value * 2)
|
|
||||||
function increment() {
|
|
||||||
count.value++
|
|
||||||
}
|
|
||||||
|
|
||||||
return { count, doubleCount, increment }
|
|
||||||
})
|
|
||||||
35
frontend/src/stores/language.ts
Normal file
35
frontend/src/stores/language.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { Language } from '@/utils/i18n'
|
||||||
|
|
||||||
|
interface LanguageState {
|
||||||
|
language: Language
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useLanguageStore = defineStore('language', {
|
||||||
|
state: (): LanguageState => ({
|
||||||
|
language: Language.English
|
||||||
|
}),
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
isEnglish: (state) => state.language === Language.English,
|
||||||
|
isSimplifiedChinese: (state) => state.language === Language.SimplifiedChinese,
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
changeLanguage(lang: Language) {
|
||||||
|
this.language = lang;
|
||||||
|
},
|
||||||
|
changeToEnglish() {
|
||||||
|
this.changeLanguage(Language.English);
|
||||||
|
},
|
||||||
|
changeToSimplifiedChinese() {
|
||||||
|
this.changeLanguage(Language.SimplifiedChinese);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
persist: {
|
||||||
|
key: 'ccn-i18n',
|
||||||
|
storage: localStorage,
|
||||||
|
pick: ['language'],
|
||||||
|
},
|
||||||
|
})
|
||||||
31
frontend/src/stores/token.ts
Normal file
31
frontend/src/stores/token.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
interface TokenState {
|
||||||
|
token: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTokenStore = defineStore('token', {
|
||||||
|
state: (): TokenState => ({
|
||||||
|
token: null,
|
||||||
|
}),
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
isLoggedIn: (state) => typeof state.token === 'string',
|
||||||
|
currentToken: (state) => state.token as string,
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
login(token: string) {
|
||||||
|
this.token = token;
|
||||||
|
},
|
||||||
|
logout() {
|
||||||
|
this.token = null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
persist: {
|
||||||
|
key: 'ccn-token',
|
||||||
|
storage: localStorage,
|
||||||
|
pick: ['token'],
|
||||||
|
},
|
||||||
|
})
|
||||||
4
frontend/src/utils/i18n.ts
Normal file
4
frontend/src/utils/i18n.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export enum Language {
|
||||||
|
English,
|
||||||
|
SimplifiedChinese,
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h1>Congratulations</h1>
|
<h1>Congratulations</h1>
|
||||||
<p>404 Not Found</p>
|
<p>This is calendar event.</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
@@ -1,8 +1,16 @@
|
|||||||
<script setup lang="ts"></script>
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h1>Congratulations</h1>
|
<div class="container" style="margin-top: 1.25rem;">
|
||||||
<p>This is home.</p>
|
<article>
|
||||||
|
<h1 class="title">coconut-leaf</h1>
|
||||||
|
<p>A light, self-host and multi-account calendar system.</p>
|
||||||
|
<p>The original intention of this system is served for yyc12345 personal use.</p>
|
||||||
|
<br />
|
||||||
|
<p>See our <a href="https://github.com/yyc12345/coconut-leaf">GitHub project</a> for the source code in detail.</p>
|
||||||
|
<p>The source code of this project is licensed under <a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL v3</a>.</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
79
frontend/src/views/Login.vue
Normal file
79
frontend/src/views/Login.vue
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useTokenStore } from '@/stores/token';
|
||||||
|
import MessageBox from '@/components/MessageBox.vue';
|
||||||
|
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 = 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>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div style="margin-top: 1.25rem; width: 100%; display: flex; justify-content: center; align-items: center;">
|
||||||
|
<div class="card" style="padding: 1.25rem;">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">User Name</label>
|
||||||
|
<div class="control has-icons-left has-icons-right">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Password</label>
|
||||||
|
<p class="control has-icons-left">
|
||||||
|
<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>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control">
|
||||||
|
<button class="button is-primary" :disabled="isLoggingIn" @click="login">Login</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MessageBox ref="messagebox"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
17
frontend/src/views/NotFound.vue
Normal file
17
frontend/src/views/NotFound.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<article>
|
||||||
|
<h1 class="title">Oops!</h1>
|
||||||
|
<p>You are wandering in the desert of coconut-leaf.</p>
|
||||||
|
<p>Please back to previous page and try again.</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
div.container {
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -33,5 +33,6 @@ export default defineConfig({
|
|||||||
rewrite: (path) => path.replace(/^\/api\//, '/'),
|
rewrite: (path) => path.replace(/^\/api\//, '/'),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user