first commit
This commit is contained in:
38
backend/app/core/config.py
Normal file
38
backend/app/core/config.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
app_name: str = 'RouterOS Backup Manager Next'
|
||||
app_env: str = 'development'
|
||||
secret_key: str = 'change-me'
|
||||
jwt_algorithm: str = 'HS256'
|
||||
access_token_expire_minutes: int = 1440
|
||||
database_url: str = 'sqlite:///./storage/routeros_backup_next.db'
|
||||
data_dir: str = './storage'
|
||||
allow_registration: bool = True
|
||||
api_prefix: str = '/api'
|
||||
timezone: str = 'Europe/Warsaw'
|
||||
default_admin_username: str = 'admin'
|
||||
default_admin_password: str = 'admin'
|
||||
smtp_starttls: bool = True
|
||||
smtp_timeout_seconds: int = 20
|
||||
cors_origins: list[str] = Field(default_factory=lambda: ['http://localhost:4200', 'http://127.0.0.1:4200'])
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file='.env',
|
||||
env_file_encoding='utf-8',
|
||||
extra='ignore',
|
||||
env_nested_delimiter='__',
|
||||
)
|
||||
|
||||
@property
|
||||
def data_path(self) -> Path:
|
||||
path = Path(self.data_dir)
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
settings = Settings()
|
||||
88
backend/app/core/cron_utils.py
Normal file
88
backend/app/core/cron_utils.py
Normal file
@@ -0,0 +1,88 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
WEEKDAY_LABELS = {
|
||||
'0': 'Sunday',
|
||||
'1': 'Monday',
|
||||
'2': 'Tuesday',
|
||||
'3': 'Wednesday',
|
||||
'4': 'Thursday',
|
||||
'5': 'Friday',
|
||||
'6': 'Saturday',
|
||||
'7': 'Sunday',
|
||||
'sun': 'Sunday',
|
||||
'mon': 'Monday',
|
||||
'tue': 'Tuesday',
|
||||
'wed': 'Wednesday',
|
||||
'thu': 'Thursday',
|
||||
'fri': 'Friday',
|
||||
'sat': 'Saturday',
|
||||
}
|
||||
|
||||
|
||||
class CronValidationError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def parse_cron_expression(expr: str, timezone_str: str) -> CronTrigger:
|
||||
expr = (expr or '').strip()
|
||||
if not expr:
|
||||
raise CronValidationError('Cron expression cannot be empty')
|
||||
|
||||
parts = expr.split()
|
||||
if len(parts) != 5:
|
||||
raise CronValidationError('Cron expression must contain exactly 5 fields')
|
||||
|
||||
minute, hour, day, month, day_of_week = parts
|
||||
try:
|
||||
return CronTrigger(
|
||||
minute=minute,
|
||||
hour=hour,
|
||||
day=day,
|
||||
month=month,
|
||||
day_of_week=day_of_week,
|
||||
timezone=ZoneInfo(timezone_str),
|
||||
)
|
||||
except Exception as exc: # pragma: no cover - APScheduler formats messages
|
||||
raise CronValidationError(str(exc)) from exc
|
||||
|
||||
|
||||
def validate_cron_expression(expr: str, timezone_str: str) -> None:
|
||||
parse_cron_expression(expr, timezone_str)
|
||||
|
||||
|
||||
def preview_next_runs(expr: str, timezone_str: str, count: int = 3) -> list[datetime]:
|
||||
trigger = parse_cron_expression(expr, timezone_str)
|
||||
now = datetime.now(ZoneInfo(timezone_str))
|
||||
previous = None
|
||||
runs: list[datetime] = []
|
||||
for _ in range(max(count, 0)):
|
||||
next_run = trigger.get_next_fire_time(previous, now)
|
||||
if not next_run:
|
||||
break
|
||||
runs.append(next_run)
|
||||
previous = next_run
|
||||
now = next_run
|
||||
return runs
|
||||
|
||||
|
||||
def describe_cron_expression(expr: str) -> str:
|
||||
expr = (expr or '').strip()
|
||||
if not expr:
|
||||
return 'Disabled'
|
||||
|
||||
parts = expr.split()
|
||||
if len(parts) != 5:
|
||||
return 'Custom cron'
|
||||
|
||||
minute, hour, day, month, day_of_week = parts
|
||||
if minute.isdigit() and hour.isdigit() and day == '*' and month == '*' and day_of_week == '*':
|
||||
return f'Every day at {int(hour):02d}:{int(minute):02d}'
|
||||
if minute.isdigit() and hour.isdigit() and day == '*' and month == '*' and day_of_week.lower() in WEEKDAY_LABELS:
|
||||
weekday = WEEKDAY_LABELS[day_of_week.lower()]
|
||||
return f'Every {weekday} at {int(hour):02d}:{int(minute):02d}'
|
||||
return 'Custom cron'
|
||||
22
backend/app/core/security.py
Normal file
22
backend/app/core/security.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from jose import jwt
|
||||
from passlib.context import CryptContext
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
|
||||
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
return pwd_context.hash(password)
|
||||
|
||||
|
||||
def create_access_token(subject: str, expires_delta: timedelta) -> str:
|
||||
expire = datetime.now(timezone.utc) + expires_delta
|
||||
to_encode = {"sub": subject, "exp": expire}
|
||||
return jwt.encode(to_encode, settings.secret_key, algorithm=settings.jwt_algorithm)
|
||||
Reference in New Issue
Block a user