fix(enhance-v4): APK QR 업로드 버그 수정 + 웹메일 라우터 패키지 경로 수정
- app_deploy.py: AppVersion.tenant_id 제거, file_size→file_size_mb, qr_data 필드 정렬 - app_deploy.py: sa_update import 방식 수정, AppDownloadLog 필드명 수정 - smart_notify.py: channels Field description 타입 오류 수정 (list→str) - contacts.py: 상대 임포트로 변경 (.auth), SQLite 독립 저장소 방식 - signature.py: 상대 임포트로 변경 (.auth), SQLite 독립 저장소 방식 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0ebac500f5
commit
b5a4f7a397
@ -5457,7 +5457,8 @@ class AppVersion(Base):
|
|||||||
android_url = Column(String(1000), nullable=True)
|
android_url = Column(String(1000), nullable=True)
|
||||||
ios_url = Column(String(1000), nullable=True)
|
ios_url = Column(String(1000), nullable=True)
|
||||||
qr_image_path = Column(String(500), nullable=True)
|
qr_image_path = Column(String(500), nullable=True)
|
||||||
landing_token = Column(String(36), nullable=True, unique=True)
|
qr_data = Column(Text, nullable=True) # base64 PNG
|
||||||
|
landing_token = Column(String(64), nullable=True, unique=True)
|
||||||
download_count = Column(Integer, default=0)
|
download_count = Column(Integer, default=0)
|
||||||
is_latest = Column(Boolean, default=False)
|
is_latest = Column(Boolean, default=False)
|
||||||
release_notes = Column(Text, nullable=True)
|
release_notes = Column(Text, nullable=True)
|
||||||
|
|||||||
@ -97,9 +97,10 @@ async def upload_apk(
|
|||||||
file_path.write_bytes(file_bytes)
|
file_path.write_bytes(file_bytes)
|
||||||
|
|
||||||
# 기존 latest 해제
|
# 기존 latest 해제
|
||||||
|
from sqlalchemy import update as sa_update
|
||||||
await db.execute(
|
await db.execute(
|
||||||
__import__('sqlalchemy', fromlist=['update']).update(AppVersion)
|
sa_update(AppVersion)
|
||||||
.where(AppVersion.tenant_id == user.tenant_id, AppVersion.is_latest == True)
|
.where(AppVersion.is_latest == True)
|
||||||
.values(is_latest=False)
|
.values(is_latest=False)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -110,11 +111,10 @@ async def upload_apk(
|
|||||||
qr_b64 = base64.b64encode(qr_bytes).decode()
|
qr_b64 = base64.b64encode(qr_bytes).decode()
|
||||||
|
|
||||||
app_ver = AppVersion(
|
app_ver = AppVersion(
|
||||||
tenant_id=user.tenant_id,
|
|
||||||
version=version,
|
version=version,
|
||||||
platform="ANDROID",
|
platform="ANDROID",
|
||||||
file_path=str(file_path),
|
file_path=str(file_path),
|
||||||
file_size=len(file_bytes),
|
file_size_mb=round(len(file_bytes) / 1024 / 1024, 2),
|
||||||
android_url=f"{BASE_URL}/api/app/download?token={token}",
|
android_url=f"{BASE_URL}/api/app/download?token={token}",
|
||||||
ios_url=ios_url or None,
|
ios_url=ios_url or None,
|
||||||
landing_token=token,
|
landing_token=token,
|
||||||
@ -147,9 +147,10 @@ async def set_app_url(
|
|||||||
user: User = Depends(require_admin_role),
|
user: User = Depends(require_admin_role),
|
||||||
):
|
):
|
||||||
"""외부 URL(EAS 빌드 등)로 QR 코드 생성."""
|
"""외부 URL(EAS 빌드 등)로 QR 코드 생성."""
|
||||||
|
from sqlalchemy import update as sa_update
|
||||||
await db.execute(
|
await db.execute(
|
||||||
__import__('sqlalchemy', fromlist=['update']).update(AppVersion)
|
sa_update(AppVersion)
|
||||||
.where(AppVersion.tenant_id == user.tenant_id, AppVersion.is_latest == True)
|
.where(AppVersion.is_latest == True)
|
||||||
.values(is_latest=False)
|
.values(is_latest=False)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -158,7 +159,6 @@ async def set_app_url(
|
|||||||
qr_bytes = _generate_qr(landing_url)
|
qr_bytes = _generate_qr(landing_url)
|
||||||
|
|
||||||
app_ver = AppVersion(
|
app_ver = AppVersion(
|
||||||
tenant_id=user.tenant_id,
|
|
||||||
version=req.version,
|
version=req.version,
|
||||||
platform="BOTH" if req.ios_url else "ANDROID",
|
platform="BOTH" if req.ios_url else "ANDROID",
|
||||||
android_url=req.android_url,
|
android_url=req.android_url,
|
||||||
@ -190,10 +190,7 @@ async def get_latest(
|
|||||||
):
|
):
|
||||||
"""최신 버전 정보 조회."""
|
"""최신 버전 정보 조회."""
|
||||||
row = await db.execute(
|
row = await db.execute(
|
||||||
select(AppVersion).where(
|
select(AppVersion).where(AppVersion.is_latest == True)
|
||||||
AppVersion.tenant_id == user.tenant_id,
|
|
||||||
AppVersion.is_latest == True,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
ver = row.scalar_one_or_none()
|
ver = row.scalar_one_or_none()
|
||||||
if not ver:
|
if not ver:
|
||||||
@ -303,8 +300,7 @@ async def app_landing(
|
|||||||
version_id=ver.id,
|
version_id=ver.id,
|
||||||
platform="IOS" if is_ios else "ANDROID",
|
platform="IOS" if is_ios else "ANDROID",
|
||||||
user_agent=request.headers.get("User-Agent", "")[:200],
|
user_agent=request.headers.get("User-Agent", "")[:200],
|
||||||
ip_addr=request.client.host if request.client else "",
|
downloaded_at=datetime.utcnow(),
|
||||||
accessed_at=datetime.utcnow(),
|
|
||||||
)
|
)
|
||||||
db.add(log)
|
db.add(log)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
@ -343,8 +339,7 @@ async def list_versions(
|
|||||||
user: User = Depends(get_current_user),
|
user: User = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
rows = await db.execute(
|
rows = await db.execute(
|
||||||
select(AppVersion).where(AppVersion.tenant_id == user.tenant_id)
|
select(AppVersion).order_by(desc(AppVersion.created_at)).limit(20)
|
||||||
.order_by(desc(AppVersion.created_at)).limit(20)
|
|
||||||
)
|
)
|
||||||
versions = rows.scalars().all()
|
versions = rows.scalars().all()
|
||||||
return [
|
return [
|
||||||
@ -353,7 +348,7 @@ async def list_versions(
|
|||||||
"download_count": v.download_count, "is_latest": v.is_latest,
|
"download_count": v.download_count, "is_latest": v.is_latest,
|
||||||
"qr_url": f"{BASE_URL}/api/app/qr?token={v.landing_token}",
|
"qr_url": f"{BASE_URL}/api/app/qr?token={v.landing_token}",
|
||||||
"landing_url": f"{BASE_URL}/api/app/landing?token={v.landing_token}",
|
"landing_url": f"{BASE_URL}/api/app/landing?token={v.landing_token}",
|
||||||
"file_size_mb": round((v.file_size or 0) / 1024 / 1024, 1),
|
"file_size_mb": round((v.file_size_mb or 0), 1),
|
||||||
"release_notes": v.release_notes,
|
"release_notes": v.release_notes,
|
||||||
"created_at": v.created_at,
|
"created_at": v.created_at,
|
||||||
}
|
}
|
||||||
@ -368,7 +363,7 @@ async def delete_version(
|
|||||||
user: User = Depends(require_admin_role),
|
user: User = Depends(require_admin_role),
|
||||||
):
|
):
|
||||||
row = await db.execute(
|
row = await db.execute(
|
||||||
select(AppVersion).where(AppVersion.id == version_id, AppVersion.tenant_id == user.tenant_id)
|
select(AppVersion).where(AppVersion.id == version_id)
|
||||||
)
|
)
|
||||||
ver = row.scalar_one_or_none()
|
ver = row.scalar_one_or_none()
|
||||||
if not ver:
|
if not ver:
|
||||||
@ -388,7 +383,7 @@ async def app_stats(
|
|||||||
user: User = Depends(get_current_user),
|
user: User = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
total = (await db.execute(
|
total = (await db.execute(
|
||||||
select(func.sum(AppVersion.download_count)).where(AppVersion.tenant_id == user.tenant_id)
|
select(func.sum(AppVersion.download_count))
|
||||||
)).scalar() or 0
|
)).scalar() or 0
|
||||||
android = (await db.execute(
|
android = (await db.execute(
|
||||||
select(func.count(AppDownloadLog.id)).where(AppDownloadLog.platform == "ANDROID")
|
select(func.count(AppDownloadLog.id)).where(AppDownloadLog.platform == "ANDROID")
|
||||||
|
|||||||
@ -42,7 +42,7 @@ class NotifyRuleCreate(BaseModel):
|
|||||||
name: str = Field(..., max_length=200)
|
name: str = Field(..., max_length=200)
|
||||||
trigger_type: str = Field(..., description="SR_CREATED|SR_UPDATED|INCIDENT|DRIFT|KPI_BREACH|CUSTOM")
|
trigger_type: str = Field(..., description="SR_CREATED|SR_UPDATED|INCIDENT|DRIFT|KPI_BREACH|CUSTOM")
|
||||||
conditions: dict = Field(default_factory=dict)
|
conditions: dict = Field(default_factory=dict)
|
||||||
channels: List[str] = Field(default_factory=list, description=["messenger","email","slack","kakao"])
|
channels: List[str] = Field(default_factory=list, description="messenger|email|slack|kakao")
|
||||||
priority_filter: str = Field("ALL", description="HIGH|MEDIUM|ALL")
|
priority_filter: str = Field("ALL", description="HIGH|MEDIUM|ALL")
|
||||||
silence_hours: Optional[List[int]] = Field(None, description="무음 시간 목록 [22,23,0,1,...,7]")
|
silence_hours: Optional[List[int]] = Field(None, description="무음 시간 목록 [22,23,0,1,...,7]")
|
||||||
digest_mode: bool = Field(False, description="묶음 발송 모드")
|
digest_mode: bool = Field(False, description="묶음 발송 모드")
|
||||||
|
|||||||
@ -1,156 +1,144 @@
|
|||||||
"""
|
"""웹메일 주소록 — SQLite 독립 저장소"""
|
||||||
웹메일 주소록 — 연락처 CRUD + 자동 저장
|
|
||||||
|
|
||||||
엔드포인트:
|
|
||||||
GET /api/mail/contacts — 주소록 목록
|
|
||||||
POST /api/mail/contacts — 연락처 추가
|
|
||||||
PUT /api/mail/contacts/{id} — 수정
|
|
||||||
DELETE /api/mail/contacts/{id} — 삭제
|
|
||||||
GET /api/mail/contacts/search — 검색
|
|
||||||
POST /api/mail/contacts/auto-save — 발신자 자동 저장
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import sqlite3
|
||||||
|
import os
|
||||||
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy import select, or_, desc
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
|
|
||||||
from auth import verify_token
|
from .auth import current_user
|
||||||
from database import get_db_session as get_db
|
|
||||||
from models import MailContact
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/mail/contacts", tags=["주소록"])
|
router = APIRouter(prefix="/api/contacts", tags=["주소록"])
|
||||||
|
|
||||||
|
DB_PATH = os.environ.get("MAIL_CONTACTS_DB", "/opt/mail/contacts.db")
|
||||||
|
|
||||||
|
|
||||||
class ContactCreate(BaseModel):
|
def _get_conn():
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
conn.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS contacts (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
email TEXT NOT NULL,
|
||||||
|
phone TEXT,
|
||||||
|
company TEXT,
|
||||||
|
use_count INTEGER DEFAULT 0,
|
||||||
|
created_at TEXT DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT DEFAULT (datetime('now'))
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_contacts_user ON contacts(username)")
|
||||||
|
conn.commit()
|
||||||
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
def _email_ok(email: str) -> bool:
|
||||||
|
return bool(re.match(r'^[^@\s]+@[^@\s]+\.[^@\s]+$', email))
|
||||||
|
|
||||||
|
|
||||||
|
class ContactIn(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
email: str
|
email: str
|
||||||
group: Optional[str] = None
|
|
||||||
phone: Optional[str] = None
|
phone: Optional[str] = None
|
||||||
note: Optional[str] = None
|
company: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class AutoSaveRequest(BaseModel):
|
@router.get("")
|
||||||
name: str
|
|
||||||
email: str
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/search")
|
|
||||||
async def search_contacts(
|
|
||||||
q: str = Query(..., min_length=1),
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
username: str = Depends(verify_token),
|
|
||||||
):
|
|
||||||
rows = await db.execute(
|
|
||||||
select(MailContact).where(
|
|
||||||
MailContact.username == username,
|
|
||||||
or_(
|
|
||||||
MailContact.name.ilike(f"%{q}%"),
|
|
||||||
MailContact.email.ilike(f"%{q}%"),
|
|
||||||
)
|
|
||||||
).order_by(desc(MailContact.use_count)).limit(10)
|
|
||||||
)
|
|
||||||
return [
|
|
||||||
{"id": c.id, "name": c.name, "email": c.email, "group": c.group}
|
|
||||||
for c in rows.scalars().all()
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/")
|
|
||||||
async def list_contacts(
|
async def list_contacts(
|
||||||
db: AsyncSession = Depends(get_db),
|
q: str = Query(None),
|
||||||
username: str = Depends(verify_token),
|
limit: int = Query(50, ge=1, le=200),
|
||||||
|
user=Depends(current_user),
|
||||||
):
|
):
|
||||||
rows = await db.execute(
|
username = user[0]
|
||||||
select(MailContact).where(MailContact.username == username)
|
conn = _get_conn()
|
||||||
.order_by(MailContact.name)
|
try:
|
||||||
)
|
if q:
|
||||||
return [
|
rows = conn.execute(
|
||||||
{"id": c.id, "name": c.name, "email": c.email,
|
"SELECT * FROM contacts WHERE username=? AND (name LIKE ? OR email LIKE ?) ORDER BY use_count DESC, name LIMIT ?",
|
||||||
"group": c.group, "phone": c.phone, "use_count": c.use_count}
|
(username, f"%{q}%", f"%{q}%", limit)
|
||||||
for c in rows.scalars().all()
|
).fetchall()
|
||||||
]
|
else:
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT * FROM contacts WHERE username=? ORDER BY use_count DESC, name LIMIT ?",
|
||||||
|
(username, limit)
|
||||||
|
).fetchall()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/")
|
@router.post("", status_code=201)
|
||||||
async def create_contact(
|
async def add_contact(body: ContactIn, user=Depends(current_user)):
|
||||||
req: ContactCreate,
|
username = user[0]
|
||||||
db: AsyncSession = Depends(get_db),
|
if not _email_ok(body.email):
|
||||||
username: str = Depends(verify_token),
|
raise HTTPException(400, "이메일 형식 오류")
|
||||||
):
|
conn = _get_conn()
|
||||||
# 중복 이메일 체크
|
try:
|
||||||
existing = await db.execute(
|
cur = conn.execute(
|
||||||
select(MailContact).where(MailContact.username == username, MailContact.email == req.email)
|
"INSERT INTO contacts (username,name,email,phone,company) VALUES (?,?,?,?,?)",
|
||||||
)
|
(username, body.name, body.email, body.phone, body.company)
|
||||||
if existing.scalar_one_or_none():
|
)
|
||||||
raise HTTPException(409, "이미 등록된 이메일")
|
conn.commit()
|
||||||
contact = MailContact(
|
row = conn.execute("SELECT * FROM contacts WHERE id=?", (cur.lastrowid,)).fetchone()
|
||||||
username=username, name=req.name, email=req.email,
|
return dict(row)
|
||||||
group=req.group, phone=req.phone, note=req.note,
|
finally:
|
||||||
use_count=0, auto_saved=False, created_at=datetime.utcnow(),
|
conn.close()
|
||||||
)
|
|
||||||
db.add(contact)
|
|
||||||
await db.commit()
|
|
||||||
await db.refresh(contact)
|
|
||||||
return {"ok": True, "id": contact.id}
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{contact_id}")
|
@router.put("/{contact_id}")
|
||||||
async def update_contact(
|
async def update_contact(contact_id: int, body: ContactIn, user=Depends(current_user)):
|
||||||
contact_id: int,
|
username = user[0]
|
||||||
req: ContactCreate,
|
conn = _get_conn()
|
||||||
db: AsyncSession = Depends(get_db),
|
try:
|
||||||
username: str = Depends(verify_token),
|
conn.execute(
|
||||||
):
|
"UPDATE contacts SET name=?,email=?,phone=?,company=?,updated_at=datetime('now') WHERE id=? AND username=?",
|
||||||
row = await db.execute(
|
(body.name, body.email, body.phone, body.company, contact_id, username)
|
||||||
select(MailContact).where(MailContact.id == contact_id, MailContact.username == username)
|
)
|
||||||
)
|
conn.commit()
|
||||||
c = row.scalar_one_or_none()
|
return {"ok": True}
|
||||||
if not c: raise HTTPException(404)
|
finally:
|
||||||
c.name = req.name; c.email = req.email; c.group = req.group
|
conn.close()
|
||||||
c.phone = req.phone; c.note = req.note
|
|
||||||
await db.commit()
|
|
||||||
return {"ok": True}
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{contact_id}")
|
@router.delete("/{contact_id}")
|
||||||
async def delete_contact(
|
async def delete_contact(contact_id: int, user=Depends(current_user)):
|
||||||
contact_id: int,
|
username = user[0]
|
||||||
db: AsyncSession = Depends(get_db),
|
conn = _get_conn()
|
||||||
username: str = Depends(verify_token),
|
try:
|
||||||
):
|
conn.execute("DELETE FROM contacts WHERE id=? AND username=?", (contact_id, username))
|
||||||
row = await db.execute(
|
conn.commit()
|
||||||
select(MailContact).where(MailContact.id == contact_id, MailContact.username == username)
|
return {"ok": True}
|
||||||
)
|
finally:
|
||||||
c = row.scalar_one_or_none()
|
conn.close()
|
||||||
if not c: raise HTTPException(404)
|
|
||||||
await db.delete(c); await db.commit()
|
|
||||||
return {"ok": True}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/auto-save")
|
@router.post("/auto-save")
|
||||||
async def auto_save_contact(
|
async def auto_save(body: ContactIn, user=Depends(current_user)):
|
||||||
req: AutoSaveRequest,
|
"""메일 발송 시 수신자 자동 저장 (중복이면 use_count 증가)."""
|
||||||
db: AsyncSession = Depends(get_db),
|
username = user[0]
|
||||||
username: str = Depends(verify_token),
|
if not _email_ok(body.email):
|
||||||
):
|
return {"ok": False}
|
||||||
"""메일 발신자 자동 저장 (사용 횟수 증가)."""
|
conn = _get_conn()
|
||||||
row = await db.execute(
|
try:
|
||||||
select(MailContact).where(MailContact.username == username, MailContact.email == req.email)
|
existing = conn.execute(
|
||||||
)
|
"SELECT id FROM contacts WHERE username=? AND email=?", (username, body.email)
|
||||||
c = row.scalar_one_or_none()
|
).fetchone()
|
||||||
if c:
|
if existing:
|
||||||
c.use_count = (c.use_count or 0) + 1
|
conn.execute(
|
||||||
else:
|
"UPDATE contacts SET use_count=use_count+1 WHERE id=?", (existing["id"],)
|
||||||
c = MailContact(
|
)
|
||||||
username=username, name=req.name, email=req.email,
|
else:
|
||||||
use_count=1, auto_saved=True, created_at=datetime.utcnow(),
|
conn.execute(
|
||||||
)
|
"INSERT INTO contacts (username,name,email) VALUES (?,?,?)",
|
||||||
db.add(c)
|
(username, body.name or body.email.split("@")[0], body.email)
|
||||||
await db.commit()
|
)
|
||||||
return {"ok": True}
|
conn.commit()
|
||||||
|
return {"ok": True}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|||||||
@ -1,71 +1,125 @@
|
|||||||
"""
|
"""웹메일 서명 관리 — SQLite 독립 저장소"""
|
||||||
웹메일 서명 편집기
|
from __future__ import annotations
|
||||||
|
|
||||||
엔드포인트:
|
import sqlite3
|
||||||
GET /api/mail/signature — 현재 서명 조회
|
import os
|
||||||
PUT /api/mail/signature — 서명 저장 (HTML)
|
|
||||||
"""
|
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy import select
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
|
|
||||||
from auth import verify_token
|
from .auth import current_user
|
||||||
from database import get_db_session as get_db
|
|
||||||
from models import MailSignature
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/mail/signature", tags=["메일 서명"])
|
router = APIRouter(prefix="/api/signature", tags=["서명"])
|
||||||
|
|
||||||
ALLOWED_TAGS = {'b', 'i', 'u', 'br', 'p', 'span', 'div', 'a', 'font', 'strong', 'em', 'h1', 'h2', 'h3', 'img'}
|
DB_PATH = os.environ.get("MAIL_CONTACTS_DB", "/opt/mail/contacts.db")
|
||||||
|
|
||||||
|
|
||||||
def _sanitize_html(html: str) -> str:
|
def _sanitize_html(html: str) -> str:
|
||||||
"""기본 HTML sanitize (script 태그 제거)."""
|
html = re.sub(r'on\w+\s*=\s*["\'][^"\']*["\']', '', html, flags=re.IGNORECASE)
|
||||||
html = re.sub(r'<script[^>]*>.*?</script>', '', html, flags=re.DOTALL | re.IGNORECASE)
|
html = re.sub(r'javascript\s*:', '', html, flags=re.IGNORECASE)
|
||||||
html = re.sub(r'on\w+="[^"]*"', '', html, flags=re.IGNORECASE)
|
return html[:5000]
|
||||||
html = re.sub(r"on\w+='[^']*'", '', html, flags=re.IGNORECASE)
|
|
||||||
return html
|
|
||||||
|
|
||||||
|
|
||||||
class SignatureUpdate(BaseModel):
|
def _get_conn():
|
||||||
html_content: str
|
conn = sqlite3.connect(DB_PATH)
|
||||||
is_active: bool = True
|
conn.row_factory = sqlite3.Row
|
||||||
|
conn.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS signatures (
|
||||||
@router.get("/")
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
async def get_signature(
|
username TEXT NOT NULL,
|
||||||
db: AsyncSession = Depends(get_db),
|
name TEXT NOT NULL,
|
||||||
username: str = Depends(verify_token),
|
html_content TEXT NOT NULL DEFAULT '',
|
||||||
):
|
is_default INTEGER DEFAULT 0,
|
||||||
row = await db.execute(select(MailSignature).where(MailSignature.username == username))
|
created_at TEXT DEFAULT (datetime('now')),
|
||||||
sig = row.scalar_one_or_none()
|
updated_at TEXT DEFAULT (datetime('now'))
|
||||||
return {
|
|
||||||
"html_content": sig.html_content if sig else "",
|
|
||||||
"is_active": sig.is_active if sig else False,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/")
|
|
||||||
async def update_signature(
|
|
||||||
req: SignatureUpdate,
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
username: str = Depends(verify_token),
|
|
||||||
):
|
|
||||||
clean_html = _sanitize_html(req.html_content)
|
|
||||||
row = await db.execute(select(MailSignature).where(MailSignature.username == username))
|
|
||||||
sig = row.scalar_one_or_none()
|
|
||||||
if sig:
|
|
||||||
sig.html_content = clean_html
|
|
||||||
sig.is_active = req.is_active
|
|
||||||
sig.updated_at = datetime.utcnow()
|
|
||||||
else:
|
|
||||||
sig = MailSignature(
|
|
||||||
username=username, html_content=clean_html,
|
|
||||||
is_active=req.is_active, updated_at=datetime.utcnow(),
|
|
||||||
)
|
)
|
||||||
db.add(sig)
|
""")
|
||||||
await db.commit()
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_sig_user ON signatures(username)")
|
||||||
return {"ok": True}
|
conn.commit()
|
||||||
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
class SigIn(BaseModel):
|
||||||
|
name: str
|
||||||
|
html_content: str
|
||||||
|
is_default: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("")
|
||||||
|
async def list_signatures(user=Depends(current_user)):
|
||||||
|
username = user[0]
|
||||||
|
conn = _get_conn()
|
||||||
|
try:
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT * FROM signatures WHERE username=? ORDER BY is_default DESC, id",
|
||||||
|
(username,)
|
||||||
|
).fetchall()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", status_code=201)
|
||||||
|
async def create_signature(body: SigIn, user=Depends(current_user)):
|
||||||
|
username = user[0]
|
||||||
|
html = _sanitize_html(body.html_content)
|
||||||
|
conn = _get_conn()
|
||||||
|
try:
|
||||||
|
if body.is_default:
|
||||||
|
conn.execute("UPDATE signatures SET is_default=0 WHERE username=?", (username,))
|
||||||
|
cur = conn.execute(
|
||||||
|
"INSERT INTO signatures (username,name,html_content,is_default) VALUES (?,?,?,?)",
|
||||||
|
(username, body.name, html, 1 if body.is_default else 0)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
row = conn.execute("SELECT * FROM signatures WHERE id=?", (cur.lastrowid,)).fetchone()
|
||||||
|
return dict(row)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{sig_id}")
|
||||||
|
async def update_signature(sig_id: int, body: SigIn, user=Depends(current_user)):
|
||||||
|
username = user[0]
|
||||||
|
html = _sanitize_html(body.html_content)
|
||||||
|
conn = _get_conn()
|
||||||
|
try:
|
||||||
|
if body.is_default:
|
||||||
|
conn.execute("UPDATE signatures SET is_default=0 WHERE username=?", (username,))
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE signatures SET name=?,html_content=?,is_default=?,updated_at=datetime('now') WHERE id=? AND username=?",
|
||||||
|
(body.name, html, 1 if body.is_default else 0, sig_id, username)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return {"ok": True}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/{sig_id}/default")
|
||||||
|
async def set_default(sig_id: int, user=Depends(current_user)):
|
||||||
|
username = user[0]
|
||||||
|
conn = _get_conn()
|
||||||
|
try:
|
||||||
|
conn.execute("UPDATE signatures SET is_default=0 WHERE username=?", (username,))
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE signatures SET is_default=1 WHERE id=? AND username=?", (sig_id, username)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return {"ok": True}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{sig_id}")
|
||||||
|
async def delete_signature(sig_id: int, user=Depends(current_user)):
|
||||||
|
username = user[0]
|
||||||
|
conn = _get_conn()
|
||||||
|
try:
|
||||||
|
conn.execute("DELETE FROM signatures WHERE id=? AND username=?", (sig_id, username))
|
||||||
|
conn.commit()
|
||||||
|
return {"ok": True}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user