1
0

6 Commits

Author SHA1 Message Date
46f2d69800 feat: modify vite config for backend frontend decouple dev 2026-05-11 22:34:16 +08:00
24790d8e69 feat: add nginx router config file for backend and frontend 2026-05-11 22:28:08 +08:00
07205396c8 refactor: change backend as the pure API backend 2026-05-11 22:20:06 +08:00
433c6cf2f8 doc: add roadmap for refactor 2026-05-06 12:19:56 +08:00
37435eeb66 refactor: introduce modern frontend 2026-04-28 15:47:32 +08:00
a0e3385670 refactor: refactor for modern layout
- split frontend and backend.
- update backend with modern Python dev strategies.
2026-04-28 13:17:54 +08:00
92 changed files with 4194 additions and 272 deletions

19
.gitignore vendored
View File

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

@@ -0,0 +1 @@
3.11

87
backend/coconut-leaf.py Normal file
View 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()

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

View File

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

View File

@@ -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
View 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"
]

View File

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

117
backend/uv.lock generated Normal file
View 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
View 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
View File

@@ -0,0 +1 @@
* text=auto eol=lf

39
frontend/.gitignore vendored Normal file
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

23
frontend/eslint.config.ts Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

49
frontend/src/App.vue Normal file
View 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
View 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')

View 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

View 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 }
})

View File

@@ -0,0 +1,8 @@
<script setup lang="ts"></script>
<template>
<h1>Congratulations</h1>
<p>This is admin.</p>
</template>
<style scoped></style>

View File

@@ -0,0 +1,8 @@
<script setup lang="ts"></script>
<template>
<h1>Congratulations</h1>
<p>This is calendar.</p>
</template>
<style scoped></style>

View File

@@ -0,0 +1,8 @@
<script setup lang="ts"></script>
<template>
<h1>Congratulations</h1>
<p>This is collection.</p>
</template>
<style scoped></style>

View File

@@ -0,0 +1,8 @@
<script setup lang="ts"></script>
<template>
<h1>Congratulations</h1>
<p>This is home.</p>
</template>
<style scoped></style>

View File

@@ -0,0 +1,8 @@
<script setup lang="ts"></script>
<template>
<h1>Congratulations</h1>
<p>404 Not Found</p>
</template>
<style scoped></style>

View File

@@ -0,0 +1,8 @@
<script setup lang="ts"></script>
<template>
<h1>Congratulations</h1>
<p>This is todo.</p>
</template>
<style scoped></style>

View 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
View File

@@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

View 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
View 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\//, '/'),
}
},
})

View File

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

View File

@@ -1,14 +0,0 @@
{
"database-type": "sqlite",
"database-config": {
"user": "",
"password": "",
"db": "",
"url": "",
"port": 3306
},
"web": {
"port": 8888
},
"debug": true
}

View File

@@ -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
View 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;
}