Compare commits
6 Commits
v1.0
...
46f2d69800
| Author | SHA1 | Date | |
|---|---|---|---|
| 46f2d69800 | |||
| 24790d8e69 | |||
| 07205396c8 | |||
| 433c6cf2f8 | |||
| 37435eeb66 | |||
| a0e3385670 |
19
.gitignore
vendored
19
.gitignore
vendored
@@ -1,16 +1,3 @@
|
|||||||
# ignore sqlite db
|
## ======== Personal ========
|
||||||
*.db
|
# Ignore VSCode
|
||||||
|
.vscode/
|
||||||
# ignore my debug setting
|
|
||||||
*.cfg
|
|
||||||
|
|
||||||
# ignore py cache
|
|
||||||
src/__pycache__
|
|
||||||
|
|
||||||
# ignore any image first
|
|
||||||
*.png
|
|
||||||
*.jpg
|
|
||||||
*.gif
|
|
||||||
|
|
||||||
# elimate vscode
|
|
||||||
.vscode
|
|
||||||
|
|||||||
10
ROADMAP.md
Normal file
10
ROADMAP.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Roadmap
|
||||||
|
|
||||||
|
1. 前后端分离,将前端静态文件和后端Python分装到两个文件夹中。同时辅助类文件夹改换位置。
|
||||||
|
1. 后端使用Astral UV重构,使得项目可以跑起来。
|
||||||
|
1. 定为1.1版本。1.0版本也要打tag然后提交。然后分叉v1-maintain分支。后续在master上开发v2。
|
||||||
|
1. 后端数据库字段重命名。
|
||||||
|
1. 前后端通信API命名格式修改。
|
||||||
|
1. 使用Vue重写前端
|
||||||
|
1. 使用Tailwind重写前端CSS
|
||||||
|
1. 使用Go重写后端。
|
||||||
19
backend/.gitignore
vendored
Normal file
19
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
## ======== Personal ========
|
||||||
|
# Database file
|
||||||
|
*.db
|
||||||
|
|
||||||
|
# Ignore setting file
|
||||||
|
coconut-leaf.toml
|
||||||
|
|
||||||
|
## ======== Python ========
|
||||||
|
# Python-generated files
|
||||||
|
__pycache__/
|
||||||
|
*.py[oc]
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
wheels/
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv
|
||||||
|
|
||||||
1
backend/.python-version
Normal file
1
backend/.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.11
|
||||||
87
backend/coconut-leaf.py
Normal file
87
backend/coconut-leaf.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
from typing import cast
|
||||||
|
from pathlib import Path
|
||||||
|
import server
|
||||||
|
import config
|
||||||
|
import utils
|
||||||
|
import database
|
||||||
|
|
||||||
|
|
||||||
|
def GetUsernamePassword() -> tuple[str, str]:
|
||||||
|
print("What is the first username of this calendar system?")
|
||||||
|
cache = input()
|
||||||
|
while not utils.IsValidUsername(cache):
|
||||||
|
print("Sorry, invalid data. Please try again.")
|
||||||
|
cache = input()
|
||||||
|
username = cache
|
||||||
|
|
||||||
|
print("Input this user password:")
|
||||||
|
cache = input()
|
||||||
|
while not utils.IsValidPassword(cache):
|
||||||
|
print("Sorry, invalid data. Please try again.")
|
||||||
|
cache = input()
|
||||||
|
password = cache
|
||||||
|
|
||||||
|
return (username, password)
|
||||||
|
|
||||||
|
|
||||||
|
def SetLoggingStyle(level: int) -> None:
|
||||||
|
logging.basicConfig(format="[%(levelname)s] %(message)s", level=level)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Set as INFO level in default first,
|
||||||
|
# and we will change it once we load the configuration file.
|
||||||
|
SetLoggingStyle(logging.INFO)
|
||||||
|
|
||||||
|
# Receive arguments
|
||||||
|
parser = ArgumentParser(
|
||||||
|
description="The server of light, self-host and multi-account calendar system."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-c",
|
||||||
|
"--config",
|
||||||
|
required=True,
|
||||||
|
type=Path,
|
||||||
|
action="store",
|
||||||
|
metavar="CONFIG_TOML",
|
||||||
|
dest="config",
|
||||||
|
help="The configuration file for coconut-leaf",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-i",
|
||||||
|
"--init",
|
||||||
|
action="store_true",
|
||||||
|
dest="init",
|
||||||
|
help="Set for initialize the calendar system",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Show splash
|
||||||
|
logging.info("Coconut-leaf")
|
||||||
|
logging.info("A light, self-host and multi-account calendar system")
|
||||||
|
logging.info("Project: https://github.com/yyc12345/coconut-leaf")
|
||||||
|
logging.info("===================")
|
||||||
|
|
||||||
|
# Load config file
|
||||||
|
try:
|
||||||
|
config.setup_config(cast(Path, args.config))
|
||||||
|
except Exception as e:
|
||||||
|
logging.critical(f"Error loading config file: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Change logging level again according to whether enable debug mode
|
||||||
|
logging_level = logging.DEBUG if config.get_config().others.debug else logging.INFO
|
||||||
|
SetLoggingStyle(logging_level)
|
||||||
|
|
||||||
|
# Initialize the calendar system if needed
|
||||||
|
if cast(bool, args.init):
|
||||||
|
gotten_data = GetUsernamePassword()
|
||||||
|
calendar = database.CalendarDatabase()
|
||||||
|
calendar.init(*gotten_data)
|
||||||
|
calendar.close()
|
||||||
|
|
||||||
|
logging.info("Staring server...")
|
||||||
|
server.run()
|
||||||
22
backend/coconut-leaf.template.toml
Normal file
22
backend/coconut-leaf.template.toml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
[database]
|
||||||
|
driver = "sqlite"
|
||||||
|
|
||||||
|
[database.config]
|
||||||
|
path = "coconut-leaf.db"
|
||||||
|
|
||||||
|
# [database]
|
||||||
|
# driver = "mysql"
|
||||||
|
#
|
||||||
|
# [database.config]
|
||||||
|
# host = "localhost"
|
||||||
|
# port = 3306
|
||||||
|
# user = "root"
|
||||||
|
# password = "password"
|
||||||
|
# database = "coconut_leaf"
|
||||||
|
|
||||||
|
[web]
|
||||||
|
port = 8848
|
||||||
|
|
||||||
|
[others]
|
||||||
|
auto-token-clean-duration = 86400
|
||||||
|
debug = true
|
||||||
130
backend/config.py
Normal file
130
backend/config.py
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import tomllib
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import StrEnum
|
||||||
|
from typing import Optional
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseDriver(StrEnum):
|
||||||
|
SQLITE = "sqlite"
|
||||||
|
MYSQL = "mysql"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_raw(raw: dict):
|
||||||
|
return DatabaseDriver(raw["driver"])
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class SqliteDatabaseConfig:
|
||||||
|
path: str
|
||||||
|
"""Database path"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_raw(raw: dict):
|
||||||
|
return SqliteDatabaseConfig(raw["path"])
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class MysqlDatabaseConfig:
|
||||||
|
host: str
|
||||||
|
"""Database host"""
|
||||||
|
port: int
|
||||||
|
"""Database port"""
|
||||||
|
user: str
|
||||||
|
"""Database user"""
|
||||||
|
password: str
|
||||||
|
"""Database password"""
|
||||||
|
database: str
|
||||||
|
"""Database name"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_raw(raw: dict):
|
||||||
|
return MysqlDatabaseConfig(
|
||||||
|
raw["host"], raw["port"], raw["user"], raw["password"], raw["database"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class DatabaseConfig:
|
||||||
|
driver: DatabaseDriver
|
||||||
|
"""Database driver"""
|
||||||
|
config: SqliteDatabaseConfig | MysqlDatabaseConfig
|
||||||
|
"""Database config"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_raw(raw: dict):
|
||||||
|
if raw["driver"] == DatabaseDriver.SQLITE:
|
||||||
|
return DatabaseConfig(
|
||||||
|
DatabaseDriver.SQLITE, SqliteDatabaseConfig.from_raw(raw["config"])
|
||||||
|
)
|
||||||
|
elif raw["driver"] == DatabaseDriver.MYSQL:
|
||||||
|
return DatabaseConfig(
|
||||||
|
DatabaseDriver.MYSQL, MysqlDatabaseConfig.from_raw(raw["config"])
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid database driver")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class WebConfig:
|
||||||
|
port: int
|
||||||
|
"""Web server port"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_raw(raw: dict):
|
||||||
|
return WebConfig(raw["port"])
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class OthersConfig:
|
||||||
|
debug: bool
|
||||||
|
"""Whether enable debug mode"""
|
||||||
|
auto_token_clean_duration: int
|
||||||
|
"""Auto token clean duration"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_raw(raw: dict):
|
||||||
|
return OthersConfig(raw["debug"], raw["auto-token-clean-duration"])
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Config:
|
||||||
|
database: DatabaseConfig
|
||||||
|
web: WebConfig
|
||||||
|
others: OthersConfig
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_raw(raw: dict):
|
||||||
|
return Config(
|
||||||
|
database=DatabaseConfig.from_raw(raw["database"]),
|
||||||
|
web=WebConfig.from_raw(raw["web"]),
|
||||||
|
others=OthersConfig.from_raw(raw["others"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_CONFIG: Optional[Config] = None
|
||||||
|
|
||||||
|
|
||||||
|
def setup_config(p: Path) -> None:
|
||||||
|
"""
|
||||||
|
Setup config by given path.
|
||||||
|
|
||||||
|
Raise exception if config file is invalid.
|
||||||
|
"""
|
||||||
|
with open(p, "rb") as f:
|
||||||
|
raw = tomllib.load(f)
|
||||||
|
|
||||||
|
global _CONFIG
|
||||||
|
_CONFIG = Config.from_raw(raw)
|
||||||
|
|
||||||
|
|
||||||
|
def get_config() -> Config:
|
||||||
|
"""
|
||||||
|
Get config instance.
|
||||||
|
|
||||||
|
Raises RuntimeError if config is not loaded.
|
||||||
|
"""
|
||||||
|
if _CONFIG is None:
|
||||||
|
raise RuntimeError("Config is not loaded. Call setup_config() first.")
|
||||||
|
else:
|
||||||
|
return _CONFIG
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
import config
|
import config
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import json
|
|
||||||
import utils
|
import utils
|
||||||
import threading
|
import threading
|
||||||
import logging
|
import logging
|
||||||
import dt
|
import dt
|
||||||
|
from typing import cast
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
def SafeDatabaseOperation(func):
|
def SafeDatabaseOperation(func):
|
||||||
def wrapper(self, *args, **kwargs):
|
def wrapper(self: 'CalendarDatabase', *args, **kwargs):
|
||||||
|
cfg = config.get_config()
|
||||||
|
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
# check database and acquire cursor
|
# check database and acquire cursor
|
||||||
try:
|
try:
|
||||||
@@ -15,16 +18,16 @@ def SafeDatabaseOperation(func):
|
|||||||
self.cursor = self.db.cursor()
|
self.cursor = self.db.cursor()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.cursor = None
|
self.cursor = None
|
||||||
if config.CustomConfig['debug']:
|
if cfg.others.debug:
|
||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
return (False, str(e), None)
|
return (False, str(e), None)
|
||||||
|
|
||||||
# do real data work
|
# do real data work
|
||||||
try:
|
try:
|
||||||
currentTime = utils.GetCurrentTimestamp()
|
currentTime = utils.GetCurrentTimestamp()
|
||||||
if currentTime - self.latestClean > config.CustomConfig['auto-token-clean-duration']:
|
if currentTime - self.latestClean > cfg.others.auto_token_clean_duration:
|
||||||
self.latestClean = currentTime
|
self.latestClean = currentTime
|
||||||
print('Cleaning outdated token...')
|
logging.info('Cleaning outdated token...')
|
||||||
self.tokenOper_clean()
|
self.tokenOper_clean()
|
||||||
|
|
||||||
result = (True, '', func(self, *args, **kwargs))
|
result = (True, '', func(self, *args, **kwargs))
|
||||||
@@ -36,13 +39,19 @@ def SafeDatabaseOperation(func):
|
|||||||
self.cursor.close()
|
self.cursor.close()
|
||||||
self.cursor = None
|
self.cursor = None
|
||||||
self.db.rollback()
|
self.db.rollback()
|
||||||
if config.CustomConfig['debug']:
|
if cfg.others.debug:
|
||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
return (False, str(e), None)
|
return (False, str(e), None)
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
class CalendarDatabase(object):
|
class CalendarDatabase:
|
||||||
|
|
||||||
|
db: sqlite3.Connection
|
||||||
|
cursor: sqlite3.Cursor
|
||||||
|
mutex: threading.Lock
|
||||||
|
latestClean: int
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.db = None
|
self.db = None
|
||||||
self.cursor = None
|
self.cursor = None
|
||||||
@@ -53,23 +62,36 @@ class CalendarDatabase(object):
|
|||||||
if (self.is_database_valid()):
|
if (self.is_database_valid()):
|
||||||
raise Exception('Databade is opened')
|
raise Exception('Databade is opened')
|
||||||
|
|
||||||
if config.CustomConfig['database-type'] == 'sqlite':
|
cfg = config.get_config()
|
||||||
self.db = sqlite3.connect(config.CustomConfig['database-config']['url'], check_same_thread = False)
|
match cfg.database.driver:
|
||||||
self.db.execute('PRAGMA encoding = "UTF-8";')
|
case config.DatabaseDriver.SQLITE:
|
||||||
self.db.execute('PRAGMA foreign_keys = ON;')
|
self.db = sqlite3.connect(cast(config.SqliteDatabaseConfig, cfg.database.config).path, check_same_thread = False)
|
||||||
elif config.CustomConfig['database-type'] == 'mysql':
|
self.db.execute('PRAGMA encoding = "UTF-8";')
|
||||||
raise Exception('Not implemented database')
|
self.db.execute('PRAGMA foreign_keys = ON;')
|
||||||
else:
|
case config.DatabaseDriver.MYSQL:
|
||||||
raise Exception('Unknow database type')
|
raise Exception('Not implemented database')
|
||||||
|
case _:
|
||||||
|
raise Exception('Unknow database type')
|
||||||
|
|
||||||
def init(self, username, password):
|
def init(self, username, password):
|
||||||
if (self.is_database_valid()):
|
if (self.is_database_valid()):
|
||||||
raise Exception('Database is opened')
|
raise Exception('Database is opened')
|
||||||
|
|
||||||
# establish tables
|
# establish tables
|
||||||
|
cfg = config.get_config()
|
||||||
|
backend_path = Path(__file__).resolve().parent
|
||||||
|
backend_sql_path = backend_path / 'sql'
|
||||||
|
match cfg.database.driver:
|
||||||
|
case config.DatabaseDriver.SQLITE:
|
||||||
|
sql_file = backend_sql_path / 'sqlite.sql'
|
||||||
|
case config.DatabaseDriver.MYSQL:
|
||||||
|
raise Exception('Not implemented database')
|
||||||
|
case _:
|
||||||
|
raise Exception('Unknow database type')
|
||||||
|
|
||||||
self.open()
|
self.open()
|
||||||
cursor = self.db.cursor()
|
cursor = self.db.cursor()
|
||||||
with open('sql/sqlite.sql', 'r', encoding='utf-8') as fsql:
|
with open(sql_file, 'r', encoding='utf-8') as fsql:
|
||||||
cursor.executescript(fsql.read())
|
cursor.executescript(fsql.read())
|
||||||
|
|
||||||
# finish init
|
# finish init
|
||||||
@@ -272,11 +294,11 @@ class CalendarDatabase(object):
|
|||||||
sqlList.append('[ccn_loopDateTimeStart] = ?')
|
sqlList.append('[ccn_loopDateTimeStart] = ?')
|
||||||
argumentsList.append(analyseData[5])
|
argumentsList.append(analyseData[5])
|
||||||
sqlList.append('[ccn_loopDateTimeEnd] = ?')
|
sqlList.append('[ccn_loopDateTimeEnd] = ?')
|
||||||
argumentsList.append(dt.ResolveLoopStr(
|
argumentsList.append(str(dt.ResolveLoopStr(
|
||||||
analyseData[8],
|
analyseData[8],
|
||||||
analyseData[5],
|
analyseData[5],
|
||||||
analyseData[7]
|
analyseData[7]
|
||||||
))
|
)))
|
||||||
|
|
||||||
# execute
|
# execute
|
||||||
argumentsList.append(uuid)
|
argumentsList.append(uuid)
|
||||||
@@ -531,8 +553,8 @@ class CalendarDatabase(object):
|
|||||||
argumentsList.append(_username)
|
argumentsList.append(_username)
|
||||||
self.cursor.execute('UPDATE user SET {} WHERE [ccn_name] = ?;'.format(', '.join(sqlList)),
|
self.cursor.execute('UPDATE user SET {} WHERE [ccn_name] = ?;'.format(', '.join(sqlList)),
|
||||||
tuple(argumentsList))
|
tuple(argumentsList))
|
||||||
print(cache)
|
logging.debug(cache)
|
||||||
print(tuple(argumentsList))
|
logging.debug(tuple(argumentsList))
|
||||||
if self.cursor.rowcount != 1:
|
if self.cursor.rowcount != 1:
|
||||||
raise Exception('Fail to update due to no matched rows or too much rows.')
|
raise Exception('Fail to update due to no matched rows or too much rows.')
|
||||||
return True
|
return True
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import time
|
|
||||||
import re
|
import re
|
||||||
|
import logging
|
||||||
|
import typing
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
import utils
|
import utils
|
||||||
|
|
||||||
@@ -13,7 +14,9 @@ MAX_TIMESTAMP = int(MAX_DATETIME.timestamp() / 60)
|
|||||||
DAY1_SPAN = 60 * 24
|
DAY1_SPAN = 60 * 24
|
||||||
DAY7_SPAN = 7 * DAY1_SPAN
|
DAY7_SPAN = 7 * DAY1_SPAN
|
||||||
|
|
||||||
def ResolveLoopStr(strl, starttime, tzoffset):
|
LoopHandle = typing.Callable[[re.Match, int, int, int], int]
|
||||||
|
|
||||||
|
def ResolveLoopStr(strl: str, starttime: int, tzoffset: int) -> int:
|
||||||
# check no loop
|
# check no loop
|
||||||
if strl == '':
|
if strl == '':
|
||||||
return starttime
|
return starttime
|
||||||
@@ -40,7 +43,7 @@ def ResolveLoopStr(strl, starttime, tzoffset):
|
|||||||
raise Exception('Invalid loopRules')
|
raise Exception('Invalid loopRules')
|
||||||
|
|
||||||
|
|
||||||
def LoopHandle_Year(searchResult, starttime, times, tzoffset):
|
def LoopHandle_Year(searchResult: re.Match, starttime: int, times: int, tzoffset: int) -> int:
|
||||||
clientDate = datetime.datetime.fromtimestamp(starttime * 60, UTCTimezone(tzoffset))
|
clientDate = datetime.datetime.fromtimestamp(starttime * 60, UTCTimezone(tzoffset))
|
||||||
isStrict = searchResult.group(1) == 'S'
|
isStrict = searchResult.group(1) == 'S'
|
||||||
yearSpan = int(searchResult.group(2))
|
yearSpan = int(searchResult.group(2))
|
||||||
@@ -52,7 +55,7 @@ def LoopHandle_Year(searchResult, starttime, times, tzoffset):
|
|||||||
if clientMonth == 2 and clientDay == 29:
|
if clientMonth == 2 and clientDay == 29:
|
||||||
if isStrict:
|
if isStrict:
|
||||||
realSpan = utils.LCM(yearSpan, 4)
|
realSpan = utils.LCM(yearSpan, 4)
|
||||||
print(realSpan)
|
logging.debug(realSpan)
|
||||||
valCache = starttime
|
valCache = starttime
|
||||||
while valCache < MAX_TIMESTAMP and times > 0:
|
while valCache < MAX_TIMESTAMP and times > 0:
|
||||||
newYear += realSpan
|
newYear += realSpan
|
||||||
@@ -71,7 +74,7 @@ def LoopHandle_Year(searchResult, starttime, times, tzoffset):
|
|||||||
val = starttime + DAY1_SPAN * (DaysCount(newYear, newMonth, newDay) - DaysCount(clientYear, clientMonth, clientDay))
|
val = starttime + DAY1_SPAN * (DaysCount(newYear, newMonth, newDay) - DaysCount(clientYear, clientMonth, clientDay))
|
||||||
return val if val < MAX_TIMESTAMP else MAX_TIMESTAMP
|
return val if val < MAX_TIMESTAMP else MAX_TIMESTAMP
|
||||||
|
|
||||||
def LoopHandle_Month(searchResult, starttime, times, tzoffset):
|
def LoopHandle_Month(searchResult: re.Match, starttime: int, times: int, tzoffset: int) -> int:
|
||||||
isStrict = searchResult.group(1) == 'S'
|
isStrict = searchResult.group(1) == 'S'
|
||||||
loopType = searchResult.group(2)
|
loopType = searchResult.group(2)
|
||||||
monthSpan = int(searchResult.group(3))
|
monthSpan = int(searchResult.group(3))
|
||||||
@@ -144,7 +147,7 @@ def LoopHandle_Month(searchResult, starttime, times, tzoffset):
|
|||||||
val = starttime + DAY1_SPAN * (DaysCount(newYear, newMonth, newDay) - DaysCount(clientYear, clientMonth, clientDay))
|
val = starttime + DAY1_SPAN * (DaysCount(newYear, newMonth, newDay) - DaysCount(clientYear, clientMonth, clientDay))
|
||||||
return val if val < MAX_TIMESTAMP else MAX_TIMESTAMP
|
return val if val < MAX_TIMESTAMP else MAX_TIMESTAMP
|
||||||
|
|
||||||
def LoopHandle_Week(searchResult, starttime, times, tzoffset):
|
def LoopHandle_Week(searchResult: re.Match, starttime: int, times: int, tzoffset: int) -> int:
|
||||||
weekOccupied = tuple(map(lambda x: x == 'T', searchResult.group(1)))
|
weekOccupied = tuple(map(lambda x: x == 'T', searchResult.group(1)))
|
||||||
weekEventCount = reduce(lambda x, y: x + (1 if y else 0), weekOccupied, 0)
|
weekEventCount = reduce(lambda x, y: x + (1 if y else 0), weekOccupied, 0)
|
||||||
if weekEventCount == 0:
|
if weekEventCount == 0:
|
||||||
@@ -170,25 +173,25 @@ def LoopHandle_Week(searchResult, starttime, times, tzoffset):
|
|||||||
val -= 1
|
val -= 1
|
||||||
return val if val < MAX_TIMESTAMP else MAX_TIMESTAMP
|
return val if val < MAX_TIMESTAMP else MAX_TIMESTAMP
|
||||||
|
|
||||||
def LoopHandle_Day(searchResult, starttime, times, tzoffset):
|
def LoopHandle_Day(searchResult: re.Match, starttime: int, times: int, tzoffset: int) -> int:
|
||||||
val = starttime + DAY1_SPAN * times * int(searchResult.group(1))
|
val = starttime + DAY1_SPAN * times * int(searchResult.group(1))
|
||||||
val -= 1
|
val -= 1
|
||||||
return val if val < MAX_TIMESTAMP else MAX_TIMESTAMP
|
return val if val < MAX_TIMESTAMP else MAX_TIMESTAMP
|
||||||
|
|
||||||
precompiledLoopRules = (
|
precompiledLoopRules: tuple[tuple[re.Pattern, LoopHandle], ...] = (
|
||||||
(re.compile(r'^Y([SR]{1})([1-9]\d*)$'), LoopHandle_Year),
|
(re.compile(r'^Y([SR]{1})([1-9]\d*)$'), LoopHandle_Year),
|
||||||
(re.compile(r'^M([SR]{1})([ABCD]{1})([1-9]\d*)$'), LoopHandle_Month),
|
(re.compile(r'^M([SR]{1})([ABCD]{1})([1-9]\d*)$'), LoopHandle_Month),
|
||||||
(re.compile(r'^W([TF]{7})([1-9]\d*)$'), LoopHandle_Week),
|
(re.compile(r'^W([TF]{7})([1-9]\d*)$'), LoopHandle_Week),
|
||||||
(re.compile(r'^D([1-9]\d*)$'), LoopHandle_Day)
|
(re.compile(r'^D([1-9]\d*)$'), LoopHandle_Day)
|
||||||
)
|
)
|
||||||
|
|
||||||
precompiledLoopStopRules = {
|
precompiledLoopStopRules: dict[str, re.Pattern] = {
|
||||||
'infinity': re.compile(r'^F$'),
|
'infinity': re.compile(r'^F$'),
|
||||||
'datetime': re.compile(r'^D([1-9]\d*|0)$'),
|
'datetime': re.compile(r'^D([1-9]\d*|0)$'),
|
||||||
'times': re.compile(r'^T([1-9]\d*)$')
|
'times': re.compile(r'^T([1-9]\d*)$')
|
||||||
}
|
}
|
||||||
|
|
||||||
def LeapYearCountEx(endYear, includeThis = False, baseYear = 1, includeBase = True):
|
def LeapYearCountEx(endYear: int, includeThis: bool = False, baseYear: int = 1, includeBase: bool = True):
|
||||||
if not includeThis:
|
if not includeThis:
|
||||||
endYear -= 1
|
endYear -= 1
|
||||||
if includeBase:
|
if includeBase:
|
||||||
@@ -204,10 +207,10 @@ def LeapYearCountEx(endYear, includeThis = False, baseYear = 1, includeBase = Tr
|
|||||||
|
|
||||||
return (endly - basely)
|
return (endly - basely)
|
||||||
|
|
||||||
def LeapYearCount(year):
|
def LeapYearCount(year: int):
|
||||||
return LeapYearCountEx(year, False, 1, True)
|
return LeapYearCountEx(year, False, 1, True)
|
||||||
|
|
||||||
def IsLeapYear(year):
|
def IsLeapYear(year: int):
|
||||||
isLeap = False
|
isLeap = False
|
||||||
if year % 4 == 0:
|
if year % 4 == 0:
|
||||||
isLeap = True
|
isLeap = True
|
||||||
@@ -217,7 +220,7 @@ def IsLeapYear(year):
|
|||||||
isLeap = True
|
isLeap = True
|
||||||
return isLeap
|
return isLeap
|
||||||
|
|
||||||
def DaysCount(year, month, day):
|
def DaysCount(year: int, month: int, day: int):
|
||||||
ly = LeapYearCountEx(year, False, 1, True)
|
ly = LeapYearCountEx(year, False, 1, True)
|
||||||
days = 365 * (year - 1)
|
days = 365 * (year - 1)
|
||||||
days += ly
|
days += ly
|
||||||
@@ -231,7 +234,7 @@ def DaysCount(year, month, day):
|
|||||||
days += day - 1
|
days += day - 1
|
||||||
return days
|
return days
|
||||||
|
|
||||||
def DayOfWeek(year, month, day):
|
def DayOfWeek(year: int, month: int, day: int):
|
||||||
# as we know, 1/1/1900 is Monday.
|
# as we know, 1/1/1900 is Monday.
|
||||||
# via this method, we can got 1/1/1 is Monday
|
# via this method, we can got 1/1/1 is Monday
|
||||||
# compute day span
|
# compute day span
|
||||||
@@ -240,7 +243,7 @@ def DayOfWeek(year, month, day):
|
|||||||
# return day of week (from 0 - 6, corresponding with python)
|
# return day of week (from 0 - 6, corresponding with python)
|
||||||
return days % 7
|
return days % 7
|
||||||
|
|
||||||
def GetDayInMonth(year, month, day):
|
def GetDayInMonth(year: int, month: int, day: int):
|
||||||
days = MonthDayCount[month - 1] + (1 if (month == 2 and IsLeapYear(year)) else 0)
|
days = MonthDayCount[month - 1] + (1 if (month == 2 and IsLeapYear(year)) else 0)
|
||||||
firstDayOfWeek = DayOfWeek(year, month, 1)
|
firstDayOfWeek = DayOfWeek(year, month, 1)
|
||||||
dayOfWeek = (firstDayOfWeek + day - 1) % 7
|
dayOfWeek = (firstDayOfWeek + day - 1) % 7
|
||||||
@@ -253,7 +256,7 @@ def GetDayInMonth(year, month, day):
|
|||||||
|
|
||||||
return (dayForwards, dayBackwards, weeksForward, dayOfWeek, weeksBackwards, dayOfWeek)
|
return (dayForwards, dayBackwards, weeksForward, dayOfWeek, weeksBackwards, dayOfWeek)
|
||||||
|
|
||||||
def GetMonthWeekStatistics(year, month):
|
def GetMonthWeekStatistics(year: int, month: int):
|
||||||
days = MonthDayCount[month - 1] + (1 if (month == 2 and IsLeapYear(year)) else 0)
|
days = MonthDayCount[month - 1] + (1 if (month == 2 and IsLeapYear(year)) else 0)
|
||||||
firstDayOfWeek = DayOfWeek(year, month, 1)
|
firstDayOfWeek = DayOfWeek(year, month, 1)
|
||||||
lastDayOfWeek = (firstDayOfWeek + days - 1) % 7
|
lastDayOfWeek = (firstDayOfWeek + days - 1) % 7
|
||||||
@@ -269,14 +272,18 @@ def GetMonthWeekStatistics(year, month):
|
|||||||
return tuple(result)
|
return tuple(result)
|
||||||
|
|
||||||
class UTCTimezone(datetime.tzinfo):
|
class UTCTimezone(datetime.tzinfo):
|
||||||
def __init__(self, offset = 0):
|
|
||||||
self._offset = offset
|
__offset: int
|
||||||
|
|
||||||
|
def __init__(self, offset: int = 0):
|
||||||
|
self.__offset = offset
|
||||||
|
|
||||||
def utcoffset(self, dt):
|
def utcoffset(self, dt):
|
||||||
return datetime.timedelta(minutes=self._offset)
|
return datetime.timedelta(minutes=self.__offset)
|
||||||
|
|
||||||
def tzname(self, dt):
|
def tzname(self, dt):
|
||||||
return 'UTC {}'.format(self._offset)
|
return 'UTC {}'.format(self.__offset)
|
||||||
|
|
||||||
def dst(self, dt):
|
def dst(self, dt):
|
||||||
return datetime.timedelta(0)
|
return datetime.timedelta(0)
|
||||||
|
|
||||||
14
backend/pyproject.toml
Normal file
14
backend/pyproject.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[project]
|
||||||
|
name = "coleaf-backend"
|
||||||
|
version = "1.1.0"
|
||||||
|
description = "The backend of coconut-leaf."
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dependencies = [
|
||||||
|
"flask==2.2.3",
|
||||||
|
]
|
||||||
|
[tool.uv]
|
||||||
|
constraint-dependencies = [
|
||||||
|
"Werkzeug==2.2.2",
|
||||||
|
"MarkupSafe==2.1.5"
|
||||||
|
]
|
||||||
@@ -1,15 +1,5 @@
|
|||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask import g
|
|
||||||
from flask import render_template
|
|
||||||
from flask import url_for
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask import abort
|
|
||||||
from flask import redirect
|
|
||||||
|
|
||||||
from functools import reduce
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
|
|
||||||
import config
|
import config
|
||||||
import database
|
import database
|
||||||
import utils
|
import utils
|
||||||
@@ -17,85 +7,17 @@ import utils
|
|||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
calendar_db = database.CalendarDatabase()
|
calendar_db = database.CalendarDatabase()
|
||||||
|
|
||||||
# render_static_resources = None
|
# region: API Route
|
||||||
|
|
||||||
# =============================================database
|
# region: Common
|
||||||
'''
|
|
||||||
def get_database():
|
|
||||||
db = getattr(g, '_database', None)
|
|
||||||
if db is None:
|
|
||||||
db = database.CalendarDatabase()
|
|
||||||
db.open()
|
|
||||||
return db
|
|
||||||
|
|
||||||
@app.teardown_appcontext
|
@app.route('/common/salt', methods=['POST'])
|
||||||
def close_database(exception):
|
|
||||||
db = getattr(g, '_database', None)
|
|
||||||
if db is not None:
|
|
||||||
db.close()
|
|
||||||
'''
|
|
||||||
|
|
||||||
# ============================================= static page route
|
|
||||||
|
|
||||||
@app.route('/', methods=['GET'])
|
|
||||||
def nospecHandle():
|
|
||||||
return redirect(url_for('web_homeHandle'))
|
|
||||||
|
|
||||||
@app.route('/web/home', methods=['GET'])
|
|
||||||
def web_homeHandle():
|
|
||||||
# UpdateStaticResources()
|
|
||||||
return render_template("home.html")
|
|
||||||
|
|
||||||
@app.route('/web/calendar', methods=['GET'])
|
|
||||||
def web_calendarHandle():
|
|
||||||
# UpdateStaticResources()
|
|
||||||
return render_template("calendar.html")
|
|
||||||
|
|
||||||
@app.route('/web/todo', methods=['GET'])
|
|
||||||
def web_todoHandle():
|
|
||||||
# UpdateStaticResources()
|
|
||||||
return render_template("todo.html")
|
|
||||||
|
|
||||||
@app.route('/web/admin', methods=['GET'])
|
|
||||||
def web_adminHandle():
|
|
||||||
# UpdateStaticResources()
|
|
||||||
return render_template("admin.html")
|
|
||||||
|
|
||||||
@app.route('/web/login', methods=['GET'])
|
|
||||||
def web_loginHandle():
|
|
||||||
# UpdateStaticResources()
|
|
||||||
return render_template("login.html")
|
|
||||||
|
|
||||||
@app.route('/web/collection', methods=['GET'])
|
|
||||||
def web_collectionHandle():
|
|
||||||
# UpdateStaticResources()
|
|
||||||
return render_template("collection.html")
|
|
||||||
|
|
||||||
@app.route('/web/eventAdd', methods=['GET'])
|
|
||||||
def web_eventAddHandle():
|
|
||||||
# UpdateStaticResources()
|
|
||||||
return render_template("event.html",
|
|
||||||
uuidPath=''
|
|
||||||
)
|
|
||||||
|
|
||||||
@app.route('/web/eventUpdate/<path:uuidPath>', methods=['GET'])
|
|
||||||
def web_eventUpdateHandle(uuidPath):
|
|
||||||
# UpdateStaticResources()
|
|
||||||
return render_template("event.html",
|
|
||||||
uuidPath = uuidPath
|
|
||||||
)
|
|
||||||
|
|
||||||
# ============================================= query page route
|
|
||||||
|
|
||||||
# ================================ common
|
|
||||||
|
|
||||||
@app.route('/api/common/salt', methods=['POST'])
|
|
||||||
def api_common_saltHandle():
|
def api_common_saltHandle():
|
||||||
return SmartDbCaller(calendar_db.common_salt,
|
return SmartDbCaller(calendar_db.common_salt,
|
||||||
(('username', str, False), ),
|
(('username', str, False), ),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
@app.route('/api/common/login', methods=['POST'])
|
@app.route('/common/login', methods=['POST'])
|
||||||
def api_common_loginHandle():
|
def api_common_loginHandle():
|
||||||
# construct client data first
|
# construct client data first
|
||||||
clientUa = request.user_agent.string
|
clientUa = request.user_agent.string
|
||||||
@@ -114,7 +36,7 @@ def api_common_loginHandle():
|
|||||||
'clientIp': clientIp
|
'clientIp': clientIp
|
||||||
})
|
})
|
||||||
|
|
||||||
@app.route('/api/common/webLogin', methods=['POST'])
|
@app.route('/common/webLogin', methods=['POST'])
|
||||||
def api_common_webLoginHandle():
|
def api_common_webLoginHandle():
|
||||||
# construct client data first
|
# construct client data first
|
||||||
clientUa = request.user_agent.string
|
clientUa = request.user_agent.string
|
||||||
@@ -133,21 +55,23 @@ def api_common_webLoginHandle():
|
|||||||
'clientIp': clientIp
|
'clientIp': clientIp
|
||||||
})
|
})
|
||||||
|
|
||||||
@app.route('/api/common/logout', methods=['POST'])
|
@app.route('/common/logout', methods=['POST'])
|
||||||
def api_common_logoutHandle():
|
def api_common_logoutHandle():
|
||||||
return SmartDbCaller(calendar_db.common_logout,
|
return SmartDbCaller(calendar_db.common_logout,
|
||||||
(('token', str, False), ),
|
(('token', str, False), ),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
@app.route('/api/common/tokenValid', methods=['POST'])
|
@app.route('/common/tokenValid', methods=['POST'])
|
||||||
def api_common_tokenValidHandle():
|
def api_common_tokenValidHandle():
|
||||||
return SmartDbCaller(calendar_db.common_tokenValid,
|
return SmartDbCaller(calendar_db.common_tokenValid,
|
||||||
(('token', str, False), ),
|
(('token', str, False), ),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
# ================================ calendar
|
# endregion
|
||||||
|
|
||||||
@app.route('/api/calendar/getFull', methods=['POST'])
|
# region: Calendar
|
||||||
|
|
||||||
|
@app.route('/calendar/getFull', methods=['POST'])
|
||||||
def api_calendar_getFullHandle():
|
def api_calendar_getFullHandle():
|
||||||
return SmartDbCaller(calendar_db.calendar_getFull,
|
return SmartDbCaller(calendar_db.calendar_getFull,
|
||||||
(('token', str, False),
|
(('token', str, False),
|
||||||
@@ -155,7 +79,7 @@ def api_calendar_getFullHandle():
|
|||||||
('endDateTime', int, False)),
|
('endDateTime', int, False)),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
@app.route('/api/calendar/getList', methods=['POST'])
|
@app.route('/calendar/getList', methods=['POST'])
|
||||||
def api_calendar_getListHandle():
|
def api_calendar_getListHandle():
|
||||||
return SmartDbCaller(calendar_db.calendar_getList,
|
return SmartDbCaller(calendar_db.calendar_getList,
|
||||||
(('token', str, False),
|
(('token', str, False),
|
||||||
@@ -163,14 +87,14 @@ def api_calendar_getListHandle():
|
|||||||
('endDateTime', int, False)),
|
('endDateTime', int, False)),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
@app.route('/api/calendar/getDetail', methods=['POST'])
|
@app.route('/calendar/getDetail', methods=['POST'])
|
||||||
def api_calendar_getDetailHandle():
|
def api_calendar_getDetailHandle():
|
||||||
return SmartDbCaller(calendar_db.calendar_getDetail,
|
return SmartDbCaller(calendar_db.calendar_getDetail,
|
||||||
(('token', str, False),
|
(('token', str, False),
|
||||||
('uuid', str, False)),
|
('uuid', str, False)),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
@app.route('/api/calendar/update', methods=['POST'])
|
@app.route('/calendar/update', methods=['POST'])
|
||||||
def api_calendar_updateHandle():
|
def api_calendar_updateHandle():
|
||||||
return SmartDbCaller(calendar_db.calendar_update,
|
return SmartDbCaller(calendar_db.calendar_update,
|
||||||
(('token', str, False),
|
(('token', str, False),
|
||||||
@@ -185,7 +109,7 @@ def api_calendar_updateHandle():
|
|||||||
('lastChange', str, False)),
|
('lastChange', str, False)),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
@app.route('/api/calendar/add', methods=['POST'])
|
@app.route('/calendar/add', methods=['POST'])
|
||||||
def api_calendar_addHandle():
|
def api_calendar_addHandle():
|
||||||
return SmartDbCaller(calendar_db.calendar_add,
|
return SmartDbCaller(calendar_db.calendar_add,
|
||||||
(('token', str, False),
|
(('token', str, False),
|
||||||
@@ -198,7 +122,7 @@ def api_calendar_addHandle():
|
|||||||
('timezoneOffset', int, False)),
|
('timezoneOffset', int, False)),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
@app.route('/api/calendar/delete', methods=['POST'])
|
@app.route('/calendar/delete', methods=['POST'])
|
||||||
def api_calendar_deleteHandle():
|
def api_calendar_deleteHandle():
|
||||||
return SmartDbCaller(calendar_db.calendar_delete,
|
return SmartDbCaller(calendar_db.calendar_delete,
|
||||||
(('token', str, False),
|
(('token', str, False),
|
||||||
@@ -206,35 +130,37 @@ def api_calendar_deleteHandle():
|
|||||||
('lastChange', str, False)),
|
('lastChange', str, False)),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
# ================================ collection
|
# endregion
|
||||||
|
|
||||||
@app.route('/api/collection/getFullOwn', methods=['POST'])
|
# region: Collection
|
||||||
|
|
||||||
|
@app.route('/collection/getFullOwn', methods=['POST'])
|
||||||
def api_collection_getFullOwnHandle():
|
def api_collection_getFullOwnHandle():
|
||||||
return SmartDbCaller(calendar_db.collection_getFullOwn,
|
return SmartDbCaller(calendar_db.collection_getFullOwn,
|
||||||
(('token', str, False), ),
|
(('token', str, False), ),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
@app.route('/api/collection/getListOwn', methods=['POST'])
|
@app.route('/collection/getListOwn', methods=['POST'])
|
||||||
def api_collection_getListOwnHandle():
|
def api_collection_getListOwnHandle():
|
||||||
return SmartDbCaller(calendar_db.collection_getListlOwn,
|
return SmartDbCaller(calendar_db.collection_getListOwn,
|
||||||
(('token', str, False), ),
|
(('token', str, False), ),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
@app.route('/api/collection/getDetailOwn', methods=['POST'])
|
@app.route('/collection/getDetailOwn', methods=['POST'])
|
||||||
def api_collection_getDetailOwnHandle():
|
def api_collection_getDetailOwnHandle():
|
||||||
return SmartDbCaller(calendar_db.collection_getDetailOwn,
|
return SmartDbCaller(calendar_db.collection_getDetailOwn,
|
||||||
(('token', str, False),
|
(('token', str, False),
|
||||||
('uuid', str, False)),
|
('uuid', str, False)),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
@app.route('/api/collection/addOwn', methods=['POST'])
|
@app.route('/collection/addOwn', methods=['POST'])
|
||||||
def api_collection_addOwnHandle():
|
def api_collection_addOwnHandle():
|
||||||
return SmartDbCaller(calendar_db.collection_addOwn,
|
return SmartDbCaller(calendar_db.collection_addOwn,
|
||||||
(('token', str, False),
|
(('token', str, False),
|
||||||
('name', str, False)),
|
('name', str, False)),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
@app.route('/api/collection/updateOwn', methods=['POST'])
|
@app.route('/collection/updateOwn', methods=['POST'])
|
||||||
def api_collection_updateOwnHandle():
|
def api_collection_updateOwnHandle():
|
||||||
return SmartDbCaller(calendar_db.collection_updateOwn,
|
return SmartDbCaller(calendar_db.collection_updateOwn,
|
||||||
(('token', str, False),
|
(('token', str, False),
|
||||||
@@ -243,7 +169,7 @@ def api_collection_updateOwnHandle():
|
|||||||
('lastChange', str, False)),
|
('lastChange', str, False)),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
@app.route('/api/collection/deleteOwn', methods=['POST'])
|
@app.route('/collection/deleteOwn', methods=['POST'])
|
||||||
def api_collection_deleteOwnHandle():
|
def api_collection_deleteOwnHandle():
|
||||||
return SmartDbCaller(calendar_db.collection_deleteOwn,
|
return SmartDbCaller(calendar_db.collection_deleteOwn,
|
||||||
(('token', str, False),
|
(('token', str, False),
|
||||||
@@ -252,14 +178,14 @@ def api_collection_deleteOwnHandle():
|
|||||||
None)
|
None)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/collection/getSharing', methods=['POST'])
|
@app.route('/collection/getSharing', methods=['POST'])
|
||||||
def api_collection_getSharingHandle():
|
def api_collection_getSharingHandle():
|
||||||
return SmartDbCaller(calendar_db.collection_getSharing,
|
return SmartDbCaller(calendar_db.collection_getSharing,
|
||||||
(('token', str, False),
|
(('token', str, False),
|
||||||
('uuid', str, False)),
|
('uuid', str, False)),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
@app.route('/api/collection/deleteSharing', methods=['POST'])
|
@app.route('/collection/deleteSharing', methods=['POST'])
|
||||||
def api_collection_deleteSharingHandle():
|
def api_collection_deleteSharingHandle():
|
||||||
return SmartDbCaller(calendar_db.collection_deleteSharing,
|
return SmartDbCaller(calendar_db.collection_deleteSharing,
|
||||||
(('token', str, False),
|
(('token', str, False),
|
||||||
@@ -268,7 +194,7 @@ def api_collection_deleteSharingHandle():
|
|||||||
('lastChange', str, False)),
|
('lastChange', str, False)),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
@app.route('/api/collection/addSharing', methods=['POST'])
|
@app.route('/collection/addSharing', methods=['POST'])
|
||||||
def api_collection_addSharingHandle():
|
def api_collection_addSharingHandle():
|
||||||
return SmartDbCaller(calendar_db.collection_addSharing,
|
return SmartDbCaller(calendar_db.collection_addSharing,
|
||||||
(('token', str, False),
|
(('token', str, False),
|
||||||
@@ -278,40 +204,42 @@ def api_collection_addSharingHandle():
|
|||||||
None)
|
None)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/collection/getShared', methods=['POST'])
|
@app.route('/collection/getShared', methods=['POST'])
|
||||||
def api_collection_getSharedHandle():
|
def api_collection_getSharedHandle():
|
||||||
return SmartDbCaller(calendar_db.collection_getShared,
|
return SmartDbCaller(calendar_db.collection_getShared,
|
||||||
(('token', str, False), ),
|
(('token', str, False), ),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
# ================================ todo
|
# endregion
|
||||||
|
|
||||||
@app.route('/api/todo/getFull', methods=['POST'])
|
# region: Todo
|
||||||
|
|
||||||
|
@app.route('/todo/getFull', methods=['POST'])
|
||||||
def api_todo_getFullHandle():
|
def api_todo_getFullHandle():
|
||||||
return SmartDbCaller(calendar_db.todo_getFull,
|
return SmartDbCaller(calendar_db.todo_getFull,
|
||||||
(('token', str, False), ),
|
(('token', str, False), ),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
@app.route('/api/todo/getList', methods=['POST'])
|
@app.route('/todo/getList', methods=['POST'])
|
||||||
def api_todo_getListHandle():
|
def api_todo_getListHandle():
|
||||||
return SmartDbCaller(calendar_db.todo_getList,
|
return SmartDbCaller(calendar_db.todo_getList,
|
||||||
(('token', str, False), ),
|
(('token', str, False), ),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
@app.route('/api/todo/getDetail', methods=['POST'])
|
@app.route('/todo/getDetail', methods=['POST'])
|
||||||
def api_todo_getDetailHandle():
|
def api_todo_getDetailHandle():
|
||||||
return SmartDbCaller(calendar_db.todo_getDetail,
|
return SmartDbCaller(calendar_db.todo_getDetail,
|
||||||
(('token', str, False),
|
(('token', str, False),
|
||||||
('uuid', str, False)),
|
('uuid', str, False)),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
@app.route('/api/todo/add', methods=['POST'])
|
@app.route('/todo/add', methods=['POST'])
|
||||||
def api_todo_addHandle():
|
def api_todo_addHandle():
|
||||||
return SmartDbCaller(calendar_db.todo_add,
|
return SmartDbCaller(calendar_db.todo_add,
|
||||||
(('token', str, False), ),
|
(('token', str, False), ),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
@app.route('/api/todo/update', methods=['POST'])
|
@app.route('/todo/update', methods=['POST'])
|
||||||
def api_todo_updateHandle():
|
def api_todo_updateHandle():
|
||||||
return SmartDbCaller(calendar_db.todo_update,
|
return SmartDbCaller(calendar_db.todo_update,
|
||||||
(('token', str, False),
|
(('token', str, False),
|
||||||
@@ -320,7 +248,7 @@ def api_todo_updateHandle():
|
|||||||
('lastChange', str, False)),
|
('lastChange', str, False)),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
@app.route('/api/todo/delete', methods=['POST'])
|
@app.route('/todo/delete', methods=['POST'])
|
||||||
def api_todo_deleteHandle():
|
def api_todo_deleteHandle():
|
||||||
return SmartDbCaller(calendar_db.todo_delete,
|
return SmartDbCaller(calendar_db.todo_delete,
|
||||||
(('token', str, False),
|
(('token', str, False),
|
||||||
@@ -328,22 +256,24 @@ def api_todo_deleteHandle():
|
|||||||
('lastChange', str, False)),
|
('lastChange', str, False)),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
# ================================ admin
|
# endregion
|
||||||
|
|
||||||
@app.route('/api/admin/get', methods=['POST'])
|
# region: Admin
|
||||||
|
|
||||||
|
@app.route('/admin/get', methods=['POST'])
|
||||||
def api_admin_getHandle():
|
def api_admin_getHandle():
|
||||||
return SmartDbCaller(calendar_db.admin_get,
|
return SmartDbCaller(calendar_db.admin_get,
|
||||||
(('token', str, False), ),
|
(('token', str, False), ),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
@app.route('/api/admin/add', methods=['POST'])
|
@app.route('/admin/add', methods=['POST'])
|
||||||
def api_admin_addHandle():
|
def api_admin_addHandle():
|
||||||
return SmartDbCaller(calendar_db.admin_add,
|
return SmartDbCaller(calendar_db.admin_add,
|
||||||
(('token', str, False),
|
(('token', str, False),
|
||||||
('username', str, False)),
|
('username', str, False)),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
@app.route('/api/admin/update', methods=['POST'])
|
@app.route('/admin/update', methods=['POST'])
|
||||||
def api_admin_updateHandle():
|
def api_admin_updateHandle():
|
||||||
return SmartDbCaller(calendar_db.admin_update,
|
return SmartDbCaller(calendar_db.admin_update,
|
||||||
(('token', str, False),
|
(('token', str, False),
|
||||||
@@ -352,60 +282,48 @@ def api_admin_updateHandle():
|
|||||||
('isAdmin', utils.Str2Bool, True)),
|
('isAdmin', utils.Str2Bool, True)),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
@app.route('/api/admin/delete', methods=['POST'])
|
@app.route('/admin/delete', methods=['POST'])
|
||||||
def api_admin_deleteHandle():
|
def api_admin_deleteHandle():
|
||||||
return SmartDbCaller(calendar_db.admin_delete,
|
return SmartDbCaller(calendar_db.admin_delete,
|
||||||
(('token', str, False),
|
(('token', str, False),
|
||||||
('username', str, False)),
|
('username', str, False)),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
# ================================ profile
|
# endregion
|
||||||
|
|
||||||
@app.route('/api/profile/isAdmin', methods=['POST'])
|
# region: Profile
|
||||||
|
|
||||||
|
@app.route('/profile/isAdmin', methods=['POST'])
|
||||||
def api_profile_isAdminHandle():
|
def api_profile_isAdminHandle():
|
||||||
return SmartDbCaller(calendar_db.profile_isAdmin,
|
return SmartDbCaller(calendar_db.profile_isAdmin,
|
||||||
(('token', str, False), ),
|
(('token', str, False), ),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
@app.route('/api/profile/changePassword', methods=['POST'])
|
@app.route('/profile/changePassword', methods=['POST'])
|
||||||
def api_profile_changePasswordHandle():
|
def api_profile_changePasswordHandle():
|
||||||
return SmartDbCaller(calendar_db.profile_changePassword,
|
return SmartDbCaller(calendar_db.profile_changePassword,
|
||||||
(('token', str, False),
|
(('token', str, False),
|
||||||
('password', str, False)),
|
('password', str, False)),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
@app.route('/api/profile/getToken', methods=['POST'])
|
@app.route('/profile/getToken', methods=['POST'])
|
||||||
def api_profile_getTokenHandle():
|
def api_profile_getTokenHandle():
|
||||||
return SmartDbCaller(calendar_db.profile_getToken,
|
return SmartDbCaller(calendar_db.profile_getToken,
|
||||||
(('token', str, False), ),
|
(('token', str, False), ),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
@app.route('/api/profile/deleteToken', methods=['POST'])
|
@app.route('/profile/deleteToken', methods=['POST'])
|
||||||
def api_profile_deleteTokenHandle():
|
def api_profile_deleteTokenHandle():
|
||||||
return SmartDbCaller(calendar_db.profile_deleteToken,
|
return SmartDbCaller(calendar_db.profile_deleteToken,
|
||||||
(('token', str, False),
|
(('token', str, False),
|
||||||
('deleteToken', str, False)),
|
('deleteToken', str, False)),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
# =============================================main run
|
# endregion
|
||||||
|
|
||||||
'''
|
# endregion
|
||||||
def UpdateStaticResources():
|
|
||||||
global render_static_resources
|
|
||||||
if render_static_resources is not None:
|
|
||||||
return
|
|
||||||
|
|
||||||
render_static_resources = {
|
# region: Misc Functions
|
||||||
'url_js_localStorageAssist': url_for('static', filename='js/localStorageAssist.js'),
|
|
||||||
'url_js_i18n': url_for('static', filename='js/i18n.js'),
|
|
||||||
'url_js_api': url_for('static', filename='js/api.js'),
|
|
||||||
'url_js_headerNav': url_for('static', filename='js/headerNav.js'),
|
|
||||||
|
|
||||||
'url_tmpl_headerNac': url_for('static', filename='tmpl/headerNav.tmpl'),
|
|
||||||
|
|
||||||
'url_js_pageHome': url_for('static', filename='js/page/home.js')
|
|
||||||
}
|
|
||||||
'''
|
|
||||||
|
|
||||||
def SmartDbCaller(dbMethod, paramTuple, extraDict):
|
def SmartDbCaller(dbMethod, paramTuple, extraDict):
|
||||||
result = (False, 'Invalid parameter', None)
|
result = (False, 'Invalid parameter', None)
|
||||||
@@ -446,6 +364,7 @@ def ConstructResponseBody(returnedTuple):
|
|||||||
|
|
||||||
def run():
|
def run():
|
||||||
calendar_db.open()
|
calendar_db.open()
|
||||||
app.run(port=config.CustomConfig['web']['port'])
|
app.run(port=config.get_config().web.port)
|
||||||
calendar_db.close()
|
calendar_db.close()
|
||||||
|
|
||||||
|
# endregion
|
||||||
0
backend/sql/mysql.sql
Normal file
0
backend/sql/mysql.sql
Normal file
117
backend/uv.lock
generated
Normal file
117
backend/uv.lock
generated
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 2
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
|
||||||
|
[manifest]
|
||||||
|
constraints = [
|
||||||
|
{ name = "markupsafe", specifier = "==2.1.5" },
|
||||||
|
{ name = "werkzeug", specifier = "==2.2.2" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.3.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "coleaf-backend"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "flask" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [{ name = "flask", specifier = "==2.2.3" }]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flask"
|
||||||
|
version = "2.2.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "click" },
|
||||||
|
{ name = "itsdangerous" },
|
||||||
|
{ name = "jinja2" },
|
||||||
|
{ name = "werkzeug" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e8/5c/ff9047989bd995b1098d14b03013f160225db2282925b517bb4a967752ee/Flask-2.2.3.tar.gz", hash = "sha256:7eb373984bf1c770023fce9db164ed0c3353cd0b53f130f4693da0ca756a2e6d", size = 697599, upload-time = "2023-02-15T22:43:57.265Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/9c/a3542594ce4973786236a1b7b702b8ca81dbf40ea270f0f96284f0c27348/Flask-2.2.3-py3-none-any.whl", hash = "sha256:c0bec9477df1cb867e5a67c9e1ab758de9cb4a3e52dd70681f59fa40a62b3f2d", size = 101839, upload-time = "2023-02-15T22:43:55.501Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itsdangerous"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jinja2"
|
||||||
|
version = "3.1.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "markupsafe" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markupsafe"
|
||||||
|
version = "2.1.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384, upload-time = "2024-02-02T16:31:22.863Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", size = 18219, upload-time = "2024-02-02T16:30:19.988Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", size = 14098, upload-time = "2024-02-02T16:30:21.063Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", size = 29014, upload-time = "2024-02-02T16:30:22.926Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/18/c30da5e7a0e7f4603abfc6780574131221d9148f323752c2755d48abad30/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", size = 28220, upload-time = "2024-02-02T16:30:24.76Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/40/2e73e7d532d030b1e41180807a80d564eda53babaf04d65e15c1cf897e40/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", size = 27756, upload-time = "2024-02-02T16:30:25.877Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/46/5dca760547e8c59c5311b332f70605d24c99d1303dd9a6e1fc3ed0d73561/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", size = 33988, upload-time = "2024-02-02T16:30:26.935Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/c5/27febe918ac36397919cd4a67d5579cbbfa8da027fa1238af6285bb368ea/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", size = 32718, upload-time = "2024-02-02T16:30:28.111Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/81/56e567126a2c2bc2684d6391332e357589a96a76cb9f8e5052d85cb0ead8/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", size = 33317, upload-time = "2024-02-02T16:30:29.214Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/0b/23f4b2470accb53285c613a3ab9ec19dc944eaf53592cb6d9e2af8aa24cc/MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", size = 16670, upload-time = "2024-02-02T16:30:30.915Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/a2/c78a06a9ec6d04b3445a949615c4c7ed86a0b2eb68e44e7541b9d57067cc/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", size = 17224, upload-time = "2024-02-02T16:30:32.09Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215, upload-time = "2024-02-02T16:30:33.081Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069, upload-time = "2024-02-02T16:30:34.148Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452, upload-time = "2024-02-02T16:30:35.149Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462, upload-time = "2024-02-02T16:30:36.166Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869, upload-time = "2024-02-02T16:30:37.834Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906, upload-time = "2024-02-02T16:30:39.366Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296, upload-time = "2024-02-02T16:30:40.413Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038, upload-time = "2024-02-02T16:30:42.243Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572, upload-time = "2024-02-02T16:30:43.326Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127, upload-time = "2024-02-02T16:30:44.418Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "werkzeug"
|
||||||
|
version = "2.2.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "markupsafe" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f8/c1/1c8e539f040acd80f844c69a5ef8e2fccdf8b442dabb969e497b55d544e1/Werkzeug-2.2.2.tar.gz", hash = "sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f", size = 844378, upload-time = "2022-08-08T21:44:15.376Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/27/be6ddbcf60115305205de79c29004a0c6bc53cec814f733467b1bb89386d/Werkzeug-2.2.2-py3-none-any.whl", hash = "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5", size = 232700, upload-time = "2022-08-08T21:44:13.251Z" },
|
||||||
|
]
|
||||||
8
frontend/.editorconfig
Normal file
8
frontend/.editorconfig
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
|
||||||
|
charset = utf-8
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
end_of_line = lf
|
||||||
|
max_line_length = 100
|
||||||
1
frontend/.gitattributes
vendored
Normal file
1
frontend/.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
39
frontend/.gitignore
vendored
Normal file
39
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Cypress
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Vitest
|
||||||
|
__screenshots__/
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
*.timestamp-*-*.mjs
|
||||||
10
frontend/.oxlintrc.json
Normal file
10
frontend/.oxlintrc.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||||
|
"plugins": ["eslint", "typescript", "unicorn", "oxc", "vue"],
|
||||||
|
"env": {
|
||||||
|
"browser": true
|
||||||
|
},
|
||||||
|
"categories": {
|
||||||
|
"correctness": "error"
|
||||||
|
}
|
||||||
|
}
|
||||||
48
frontend/README.md
Normal file
48
frontend/README.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# coleaf-frontend
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||||
|
|
||||||
|
## Recommended Browser Setup
|
||||||
|
|
||||||
|
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
|
||||||
|
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
|
||||||
|
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
|
||||||
|
- Firefox:
|
||||||
|
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
|
||||||
|
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
|
||||||
|
|
||||||
|
## Type Support for `.vue` Imports in TS
|
||||||
|
|
||||||
|
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
|
||||||
|
|
||||||
|
## Customize configuration
|
||||||
|
|
||||||
|
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||||
|
|
||||||
|
## Project Setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Hot-Reload for Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type-Check, Compile and Minify for Production
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lint with [ESLint](https://eslint.org/)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm lint
|
||||||
|
```
|
||||||
1
frontend/env.d.ts
vendored
Normal file
1
frontend/env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
23
frontend/eslint.config.ts
Normal file
23
frontend/eslint.config.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { globalIgnores } from 'eslint/config'
|
||||||
|
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
|
||||||
|
import pluginVue from 'eslint-plugin-vue'
|
||||||
|
import pluginOxlint from 'eslint-plugin-oxlint'
|
||||||
|
|
||||||
|
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
|
||||||
|
// import { configureVueProject } from '@vue/eslint-config-typescript'
|
||||||
|
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
|
||||||
|
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
|
||||||
|
|
||||||
|
export default defineConfigWithVueTs(
|
||||||
|
{
|
||||||
|
name: 'app/files-to-lint',
|
||||||
|
files: ['**/*.{vue,ts,mts,tsx}'],
|
||||||
|
},
|
||||||
|
|
||||||
|
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
|
||||||
|
|
||||||
|
...pluginVue.configs['flat/essential'],
|
||||||
|
vueTsConfigs.recommended,
|
||||||
|
|
||||||
|
...pluginOxlint.buildFromOxlintConfigFile('.oxlintrc.json'),
|
||||||
|
)
|
||||||
16
frontend/index.html
Normal file
16
frontend/index.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/favicon.ico">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>coconut-leaf</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
44
frontend/package.json
Normal file
44
frontend/package.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "coleaf-frontend",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "run-p type-check \"build-only {@}\" --",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"build-only": "vite build",
|
||||||
|
"type-check": "vue-tsc --build",
|
||||||
|
"lint": "run-s lint:*",
|
||||||
|
"lint:oxlint": "oxlint . --fix",
|
||||||
|
"lint:eslint": "eslint . --fix --cache"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-free": "^7.2.0",
|
||||||
|
"@fortawesome/vue-fontawesome": "^3.2.0",
|
||||||
|
"bulma": "^1.0.4",
|
||||||
|
"pinia": "^3.0.4",
|
||||||
|
"vue": "^3.5.32",
|
||||||
|
"vue-router": "^5.0.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tsconfig/node24": "^24.0.4",
|
||||||
|
"@types/node": "^24.12.2",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.6",
|
||||||
|
"@vue/eslint-config-typescript": "^14.7.0",
|
||||||
|
"@vue/tsconfig": "^0.9.1",
|
||||||
|
"eslint": "^10.2.1",
|
||||||
|
"eslint-plugin-oxlint": "~1.60.0",
|
||||||
|
"eslint-plugin-vue": "~10.8.0",
|
||||||
|
"jiti": "^2.6.1",
|
||||||
|
"npm-run-all2": "^8.0.4",
|
||||||
|
"oxlint": "~1.60.0",
|
||||||
|
"typescript": "~6.0.0",
|
||||||
|
"vite": "^8.0.8",
|
||||||
|
"vite-plugin-vue-devtools": "^8.1.1",
|
||||||
|
"vue-tsc": "^3.2.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
3168
frontend/pnpm-lock.yaml
generated
Normal file
3168
frontend/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
0
frontend/public/index.css
Normal file
0
frontend/public/index.css
Normal file
49
frontend/src/App.vue
Normal file
49
frontend/src/App.vue
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<nav class="navbar has-shadow is-spaced bd-navbar" role="navigation" aria-label="main navigation">
|
||||||
|
<div class="navbar-brand">
|
||||||
|
<router-link class="navbar-item" to="/">
|
||||||
|
<img src="/public/favicon.ico"><b style="margin:0 0 0 14px;">coconut-leaf</b>
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<a role="button" class="navbar-burger burger" 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>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="coleaf-navbar" class="navbar-menu">
|
||||||
|
<div class="navbar-start">
|
||||||
|
<router-link class="navbar-item" to="/home">Home</router-link>
|
||||||
|
<router-link class="navbar-item" to="/collection">Collection</router-link>
|
||||||
|
<router-link class="navbar-item" to="/calendar">Calendar</router-link>
|
||||||
|
<router-link class="navbar-item" to="/todo">Todo</router-link>
|
||||||
|
<router-link class="navbar-item" to="/admin">Admin</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="navbar-end">
|
||||||
|
<p class="navbar-item">
|
||||||
|
<a class="button is-primary" href="/login">Login</a>
|
||||||
|
</p>
|
||||||
|
<p class="navbar-item">
|
||||||
|
<a class="button is-primary">Logout</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
|
<a class="navbar-link"></a>
|
||||||
|
<div class="navbar-dropdown">
|
||||||
|
<a language="en-US" class="navbar-item">English</a>
|
||||||
|
<a language="zh-CN" class="navbar-item">简体中文</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<!-- The output result of router -->
|
||||||
|
<router-view></router-view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
12
frontend/src/main.ts
Normal file
12
frontend/src/main.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(createPinia())
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
29
frontend/src/router/index.ts
Normal file
29
frontend/src/router/index.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
|
import Home from '../views/Home.vue'
|
||||||
|
import Collection from '@/views/Collection.vue'
|
||||||
|
import Calendar from '@/views/Calendar.vue'
|
||||||
|
import Todo from '@/views/Todo.vue'
|
||||||
|
import Admin from '@/views/Admin.vue'
|
||||||
|
|
||||||
|
import Page404 from '@/views/Page404.vue'
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{ path: '/home', component: Home },
|
||||||
|
{ path: '/collection', component: Collection },
|
||||||
|
{ path: '/calendar', component: Calendar},
|
||||||
|
{ path: '/todo', component: Todo},
|
||||||
|
{ path: '/admin', component: Admin },
|
||||||
|
|
||||||
|
{ path: '/404', component: Page404 },
|
||||||
|
|
||||||
|
{ path: '/', redirect: '/home' },
|
||||||
|
{ path: '/:pathMatch(.*)*', redirect: '/404' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes: routes,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router
|
||||||
12
frontend/src/stores/counter.ts
Normal file
12
frontend/src/stores/counter.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
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 }
|
||||||
|
})
|
||||||
8
frontend/src/views/Admin.vue
Normal file
8
frontend/src/views/Admin.vue
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1>Congratulations</h1>
|
||||||
|
<p>This is admin.</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
8
frontend/src/views/Calendar.vue
Normal file
8
frontend/src/views/Calendar.vue
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1>Congratulations</h1>
|
||||||
|
<p>This is calendar.</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
8
frontend/src/views/Collection.vue
Normal file
8
frontend/src/views/Collection.vue
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1>Congratulations</h1>
|
||||||
|
<p>This is collection.</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
8
frontend/src/views/Home.vue
Normal file
8
frontend/src/views/Home.vue
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1>Congratulations</h1>
|
||||||
|
<p>This is home.</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
8
frontend/src/views/Page404.vue
Normal file
8
frontend/src/views/Page404.vue
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1>Congratulations</h1>
|
||||||
|
<p>404 Not Found</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
8
frontend/src/views/Todo.vue
Normal file
8
frontend/src/views/Todo.vue
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1>Congratulations</h1>
|
||||||
|
<p>This is todo.</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
18
frontend/tsconfig.app.json
Normal file
18
frontend/tsconfig.app.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||||
|
"exclude": ["src/**/__tests__/*"],
|
||||||
|
"compilerOptions": {
|
||||||
|
// Extra safety for array and object lookups, but may have false positives.
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
|
||||||
|
// Path mapping for cleaner imports.
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
|
||||||
|
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
|
||||||
|
// Specified here to keep it out of the root directory.
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
frontend/tsconfig.json
Normal file
11
frontend/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
27
frontend/tsconfig.node.json
Normal file
27
frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// TSConfig for modules that run in Node.js environment via either transpilation or type-stripping.
|
||||||
|
{
|
||||||
|
"extends": "@tsconfig/node24/tsconfig.json",
|
||||||
|
"include": [
|
||||||
|
"vite.config.*",
|
||||||
|
"vitest.config.*",
|
||||||
|
"cypress.config.*",
|
||||||
|
"playwright.config.*",
|
||||||
|
"eslint.config.*"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
// Most tools use transpilation instead of Node.js's native type-stripping.
|
||||||
|
// Bundler mode provides a smoother developer experience.
|
||||||
|
"module": "preserve",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
|
||||||
|
// Include Node.js types and avoid accidentally including other `@types/*` packages.
|
||||||
|
"types": ["node"],
|
||||||
|
|
||||||
|
// Disable emitting output during `vue-tsc --build`, which is used for type-checking only.
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
|
||||||
|
// Specified here to keep it out of the root directory.
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
|
||||||
|
}
|
||||||
|
}
|
||||||
37
frontend/vite.config.ts
Normal file
37
frontend/vite.config.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
vueDevTools(),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 项目基础路径(对应 Nginx 路由到的 /web 前缀)
|
||||||
|
base: '/web/',
|
||||||
|
|
||||||
|
server: {
|
||||||
|
port: 5173, // 开发服务器默认端口
|
||||||
|
open: '/web/', // 启动时自动打开 /web 路径
|
||||||
|
|
||||||
|
// 核心:代理配置(对应 Nginx 的 /api -> Go 服务)
|
||||||
|
proxy: {
|
||||||
|
// 精细控制路由,对齐生产环境
|
||||||
|
'^/api/(.*)$': {
|
||||||
|
target: 'http://127.0.0.1:8848',
|
||||||
|
changeOrigin: true,
|
||||||
|
// 路径重写/api/user -> /user
|
||||||
|
rewrite: (path) => path.replace(/^\/api\//, '/'),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
})
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import os
|
|
||||||
import sys
|
|
||||||
import getopt
|
|
||||||
import server
|
|
||||||
import config
|
|
||||||
import utils
|
|
||||||
import database
|
|
||||||
|
|
||||||
def GetUsernamePassword():
|
|
||||||
print('What is the first username of this calendar system?')
|
|
||||||
cache = input()
|
|
||||||
while(not utils.IsValidUsername(cache)):
|
|
||||||
print('Sorry, invalid data. Please try again.')
|
|
||||||
cache = input()
|
|
||||||
username = cache
|
|
||||||
|
|
||||||
print("Input this user password:")
|
|
||||||
cache = input()
|
|
||||||
while(not utils.IsValidPassword(cache)):
|
|
||||||
print('Sorry, invalid data. Please try again.')
|
|
||||||
cache = input()
|
|
||||||
password = cache
|
|
||||||
|
|
||||||
return (username, password)
|
|
||||||
|
|
||||||
print('Coconut-leaf')
|
|
||||||
print('A self-host, multi-account calendar system.')
|
|
||||||
print('Project: https://github.com/yyc12345/coconut-leaf')
|
|
||||||
print('===================')
|
|
||||||
|
|
||||||
# process args
|
|
||||||
# preset init value
|
|
||||||
need_init = False
|
|
||||||
try:
|
|
||||||
opts, args = getopt.getopt(sys.argv[1:], "hi")
|
|
||||||
except getopt.GetoptError:
|
|
||||||
print('Wrong arguments!')
|
|
||||||
print('python coconut-leaf.py [-i] [-h]')
|
|
||||||
sys.exit(1)
|
|
||||||
for opt, arg in opts:
|
|
||||||
if opt == '-h':
|
|
||||||
print('python coconut-leaf.py [-i]')
|
|
||||||
sys.exit(0)
|
|
||||||
elif opt == '-i':
|
|
||||||
need_init = True
|
|
||||||
|
|
||||||
if need_init:
|
|
||||||
gotten_data = GetUsernamePassword()
|
|
||||||
calendar = database.CalendarDatabase()
|
|
||||||
calendar.init(*gotten_data)
|
|
||||||
calendar.close()
|
|
||||||
|
|
||||||
print('Staring server...')
|
|
||||||
server.run()
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"database-type": "sqlite",
|
|
||||||
"database-config": {
|
|
||||||
"user": "",
|
|
||||||
"password": "",
|
|
||||||
"db": "",
|
|
||||||
"url": "",
|
|
||||||
"port": 3306
|
|
||||||
},
|
|
||||||
"web": {
|
|
||||||
"port": 8888
|
|
||||||
},
|
|
||||||
"debug": true
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import json
|
|
||||||
|
|
||||||
CustomConfig = None
|
|
||||||
|
|
||||||
# read cfg
|
|
||||||
with open('config.cfg', 'r', encoding='utf-8') as f:
|
|
||||||
CustomConfig = json.load(f)
|
|
||||||
61
tools/.nginx.conf
Normal file
61
tools/.nginx.conf
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# ============================
|
||||||
|
# 路由 1: /web -> 静态文件
|
||||||
|
# ============================
|
||||||
|
location /web {
|
||||||
|
# 使用 alias 精确映射
|
||||||
|
# 请求 /web/index.html -> /var/www/static/index.html
|
||||||
|
alias /var/www/static;
|
||||||
|
|
||||||
|
# 静态文件优化
|
||||||
|
expires 7d;
|
||||||
|
add_header Cache-Control "public, max-age=604800";
|
||||||
|
|
||||||
|
# 尝试返回文件,不存在则返回404(避免落入其他location)
|
||||||
|
try_files $uri $uri/ =404;
|
||||||
|
|
||||||
|
# 可选:启用 gzip 压缩
|
||||||
|
gzip_static on;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================
|
||||||
|
# 路由 2: /api -> Go 程序 (8848端口)
|
||||||
|
# ============================
|
||||||
|
location /api {
|
||||||
|
# 反向代理到本地 Go 服务
|
||||||
|
proxy_pass http://127.0.0.1:8848;
|
||||||
|
|
||||||
|
# 重要:保留原始请求头
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# WebSocket 支持(如果 Go 程序需要)
|
||||||
|
# proxy_http_version 1.1;
|
||||||
|
# proxy_set_header Upgrade $http_upgrade;
|
||||||
|
# proxy_set_header Connection "upgrade";
|
||||||
|
|
||||||
|
# 超时设置(根据业务调整)
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
|
||||||
|
# 缓冲设置(可选,大文件上传时注意调整)
|
||||||
|
proxy_buffering on;
|
||||||
|
proxy_buffer_size 4k;
|
||||||
|
proxy_buffers 8 4k;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================
|
||||||
|
# 可选:根路径处理
|
||||||
|
# ============================
|
||||||
|
location = / {
|
||||||
|
# 重定向到 /web
|
||||||
|
return 302 /web/;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 禁止访问隐藏文件
|
||||||
|
location ~ /\. {
|
||||||
|
deny all;
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user