readme
This commit is contained in:
@@ -46,6 +46,15 @@ ENHANCED_MEETUPS: list[dict[str, Any]] = [
|
||||
],
|
||||
"isUpcoming": idx % 5 != 0,
|
||||
"status": "published",
|
||||
"mode": "online" if idx % 6 == 0 else "hybrid" if idx % 4 == 0 else "offline",
|
||||
"maxAttendees": 80 if idx % 6 == 0 else 36 if idx % 4 == 0 else 24,
|
||||
"organizer": f"{city['name']}城市主理人",
|
||||
"accessLevel": "vip" if idx % 6 == 0 else "members" if idx % 4 == 0 else "public",
|
||||
"meetingProvider": "mirotalk" if idx % 4 == 0 or idx % 6 == 0 else "",
|
||||
"mirotalkRoom": f"nomadcna-{city['slug']}-remote-roundtable" if idx % 4 == 0 or idx % 6 == 0 else "",
|
||||
"meetingUrl": "",
|
||||
"replayUrl": "",
|
||||
"features": ["group_video", "screen_share", "whiteboard", "host_controls"] if idx % 4 == 0 or idx % 6 == 0 else [],
|
||||
}
|
||||
for idx, city in enumerate(CITIES, start=1)
|
||||
]
|
||||
|
||||
@@ -188,6 +188,7 @@ COLLECTIONS: dict[str, list[dict[str, Any]]] = {
|
||||
text("filename", max=255),
|
||||
text("contentType", max=120),
|
||||
number("size", only_int=True),
|
||||
text("objectKey", max=500),
|
||||
],
|
||||
"meetups": [
|
||||
text("city", required=True, max=120),
|
||||
@@ -203,6 +204,15 @@ COLLECTIONS: dict[str, list[dict[str, Any]]] = {
|
||||
json_field("attendees"),
|
||||
boolean("isUpcoming"),
|
||||
text("status", max=40),
|
||||
text("mode", max=40),
|
||||
number("maxAttendees", only_int=True),
|
||||
text("organizer", max=120),
|
||||
text("accessLevel", max=40),
|
||||
text("meetingProvider", max=80),
|
||||
text("mirotalkRoom", max=160),
|
||||
text("meetingUrl", max=500),
|
||||
text("replayUrl", max=500),
|
||||
json_field("features"),
|
||||
],
|
||||
"discussions": [
|
||||
text("title", required=True, max=255),
|
||||
|
||||
@@ -14,11 +14,12 @@ from typing import Any
|
||||
import httpx
|
||||
from fastapi import FastAPI, File, HTTPException, Request, Response, UploadFile
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.responses import Response as FastAPIResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .minio_storage import upload_bytes_to_minio, upload_file_to_minio
|
||||
from .payment import XorPayProvider, ZPayProvider, provider_for
|
||||
from .pb_client import PocketBaseError, pb, pb_quote
|
||||
from .settings import get_settings
|
||||
@@ -229,7 +230,13 @@ async def find_membership(user_id: str) -> dict[str, Any] | None:
|
||||
|
||||
def internal_auth_password() -> str:
|
||||
password = settings.email_auth_password.strip()
|
||||
return password if len(password) >= 8 else "NomadCNAEmailLogin2026!"
|
||||
return password if len(password) >= 8 else "12345678"
|
||||
|
||||
|
||||
def auth_password_for_email(email: str) -> str:
|
||||
if email.lower().strip() == settings.pb_admin_email.lower().strip():
|
||||
return settings.pb_admin_password
|
||||
return internal_auth_password()
|
||||
|
||||
|
||||
async def ensure_profile_for_user(
|
||||
@@ -260,7 +267,7 @@ async def login_with_internal_password(email: str) -> dict[str, Any]:
|
||||
return await pb.request(
|
||||
"POST",
|
||||
"/api/collections/users/auth-with-password",
|
||||
json={"identity": email, "password": internal_auth_password()},
|
||||
json={"identity": email, "password": auth_password_for_email(email)},
|
||||
)
|
||||
|
||||
|
||||
@@ -276,8 +283,8 @@ async def ensure_user(
|
||||
existing = await find_user_by_email(normalized)
|
||||
if existing:
|
||||
update_payload: dict[str, Any] = {
|
||||
"password": internal_auth_password(),
|
||||
"passwordConfirm": internal_auth_password(),
|
||||
"password": auth_password_for_email(normalized),
|
||||
"passwordConfirm": auth_password_for_email(normalized),
|
||||
}
|
||||
if name and not existing.get("name"):
|
||||
update_payload["name"] = name
|
||||
@@ -295,8 +302,8 @@ async def ensure_user(
|
||||
"email": normalized,
|
||||
"emailVisibility": True,
|
||||
"verified": True,
|
||||
"password": internal_auth_password(),
|
||||
"passwordConfirm": internal_auth_password(),
|
||||
"password": auth_password_for_email(normalized),
|
||||
"passwordConfirm": auth_password_for_email(normalized),
|
||||
"name": name or normalized.split("@")[0],
|
||||
},
|
||||
)
|
||||
@@ -912,24 +919,7 @@ async def upload_media(request: Request, file: UploadFile = File(...)) -> dict[s
|
||||
max_size = 200 * 1024 * 1024 if is_content_upload else 20 * 1024 * 1024
|
||||
if file.size and file.size > max_size:
|
||||
raise HTTPException(status_code=400, detail=f"文件大小不能超过 {max_size // 1024 // 1024}MB")
|
||||
suffix = Path(file.filename or "upload.bin").suffix
|
||||
name = f"{uuid.uuid4().hex}{suffix}"
|
||||
target = settings.upload_dir / name
|
||||
total = 0
|
||||
try:
|
||||
with target.open("wb") as out:
|
||||
while True:
|
||||
chunk = await file.read(1024 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
total += len(chunk)
|
||||
if total > max_size:
|
||||
raise HTTPException(status_code=400, detail=f"文件大小不能超过 {max_size // 1024 // 1024}MB")
|
||||
out.write(chunk)
|
||||
except Exception:
|
||||
target.unlink(missing_ok=True)
|
||||
raise
|
||||
url = f"{settings.upload_public_base_url}/uploads/{name}"
|
||||
upload = await upload_file_to_minio(file, purpose=purpose or "uploads", max_size=max_size)
|
||||
user_id = ""
|
||||
_, user = await current_user(request)
|
||||
user_id = user["id"] if user else ""
|
||||
@@ -937,13 +927,14 @@ async def upload_media(request: Request, file: UploadFile = File(...)) -> dict[s
|
||||
"media_assets",
|
||||
{
|
||||
"userId": user_id,
|
||||
"url": url,
|
||||
"filename": file.filename or name,
|
||||
"url": upload.url,
|
||||
"filename": file.filename or upload.object_key,
|
||||
"contentType": file.content_type or "application/octet-stream",
|
||||
"size": total,
|
||||
"size": upload.size,
|
||||
"objectKey": upload.object_key,
|
||||
},
|
||||
)
|
||||
return {"ok": True, "url": url}
|
||||
return {"ok": True, "url": upload.url, "objectKey": upload.object_key}
|
||||
|
||||
|
||||
@app.post("/api/pay", response_model=None)
|
||||
@@ -1535,6 +1526,11 @@ async def create_service_lead(payload: ServiceLeadPayload, request: Request) ->
|
||||
|
||||
@app.get("/api/downloads/{filename}")
|
||||
async def download_asset(filename: str) -> FastAPIResponse:
|
||||
async def redirect_to_minio(body: bytes, content_type: str) -> RedirectResponse:
|
||||
object_key = f"{settings.minio_upload_prefix}/downloads/{filename}"
|
||||
asset = await upload_bytes_to_minio(body, object_key=object_key, content_type=content_type)
|
||||
return RedirectResponse(asset.url, status_code=307)
|
||||
|
||||
if filename == "nomad-china-playbook.pdf":
|
||||
body = (
|
||||
"%PDF-1.4\n"
|
||||
@@ -1547,11 +1543,7 @@ async def download_asset(filename: str) -> FastAPIResponse:
|
||||
"xref\n0 5\n0000000000 65535 f \n0000000009 00000 n \n0000000058 00000 n \n0000000115 00000 n \n0000000204 00000 n \n"
|
||||
"trailer << /Root 1 0 R /Size 5 >>\nstartxref\n322\n%%EOF\n"
|
||||
).encode("latin-1")
|
||||
return FastAPIResponse(
|
||||
body,
|
||||
media_type="application/pdf",
|
||||
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
||||
)
|
||||
return await redirect_to_minio(body, "application/pdf")
|
||||
if filename == "nomadcna-app-demo.zip":
|
||||
buffer = io.BytesIO()
|
||||
with zipfile.ZipFile(buffer, "w", compression=zipfile.ZIP_DEFLATED) as archive:
|
||||
@@ -1593,11 +1585,7 @@ async def download_asset(filename: str) -> FastAPIResponse:
|
||||
""",
|
||||
)
|
||||
body = buffer.getvalue()
|
||||
return FastAPIResponse(
|
||||
body,
|
||||
media_type="application/zip",
|
||||
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
||||
)
|
||||
return await redirect_to_minio(body, "application/zip")
|
||||
raise HTTPException(status_code=404, detail="下载文件不存在")
|
||||
|
||||
|
||||
@@ -1611,9 +1599,25 @@ async def list_meetups() -> dict[str, Any]:
|
||||
async def create_meetup(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
if payload.get("date") and "T" not in str(payload["date"]):
|
||||
payload["date"] = f"{str(payload['date'])[:10]} 00:00:00.000Z"
|
||||
mode = str(payload.get("mode") or "offline").lower()
|
||||
if mode not in {"offline", "online", "hybrid"}:
|
||||
mode = "offline"
|
||||
access_level = str(payload.get("accessLevel") or "public").lower()
|
||||
if access_level not in {"public", "members", "vip", "hosts"}:
|
||||
access_level = "public"
|
||||
payload["mode"] = mode
|
||||
payload["accessLevel"] = access_level
|
||||
payload.setdefault("status", "pending")
|
||||
payload.setdefault("rsvpCount", 0)
|
||||
payload.setdefault("attendees", [])
|
||||
payload.setdefault("maxAttendees", 0)
|
||||
payload.setdefault("organizer", "NomadCNA")
|
||||
payload.setdefault("meetingProvider", "mirotalk" if mode != "offline" else "")
|
||||
payload.setdefault("mirotalkRoom", "")
|
||||
payload.setdefault("meetingUrl", "")
|
||||
payload.setdefault("replayUrl", "")
|
||||
if not isinstance(payload.get("features"), list):
|
||||
payload["features"] = []
|
||||
record = await pb.create_record("meetups", payload)
|
||||
await create_notification(
|
||||
title=f"新活动已提交:{payload.get('city', '同城')} · {payload.get('venue', '活动地点')}",
|
||||
|
||||
133
backend/minio_storage.py
Normal file
133
backend/minio_storage.py
Normal file
@@ -0,0 +1,133 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import io
|
||||
import json
|
||||
import tempfile
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import HTTPException, UploadFile
|
||||
from minio import Minio
|
||||
|
||||
from .settings import get_settings
|
||||
|
||||
|
||||
@dataclass
|
||||
class MinioUploadResult:
|
||||
url: str
|
||||
object_key: str
|
||||
size: int
|
||||
|
||||
|
||||
def _client() -> Minio:
|
||||
settings = get_settings()
|
||||
endpoint = settings.minio_endpoint
|
||||
if settings.minio_port and ":" not in endpoint:
|
||||
endpoint = f"{endpoint}:{settings.minio_port}"
|
||||
return Minio(
|
||||
endpoint,
|
||||
access_key=settings.minio_access_key,
|
||||
secret_key=settings.minio_secret_key,
|
||||
secure=settings.minio_use_ssl,
|
||||
region=settings.minio_region,
|
||||
)
|
||||
|
||||
|
||||
def _object_key(filename: str, purpose: str) -> str:
|
||||
settings = get_settings()
|
||||
suffix = Path(filename or "upload.bin").suffix.lower() or ".bin"
|
||||
day = datetime.now(timezone.utc).strftime("%Y/%m/%d")
|
||||
prefix = settings.minio_upload_prefix or "uploads"
|
||||
purpose_part = (purpose or "uploads").replace("/", "-")[:40]
|
||||
return f"{prefix}/{purpose_part}/{day}/{uuid.uuid4().hex}{suffix}"
|
||||
|
||||
|
||||
async def _ensure_bucket(client: Minio, bucket: str) -> None:
|
||||
exists = await asyncio.to_thread(client.bucket_exists, bucket)
|
||||
if not exists:
|
||||
await asyncio.to_thread(client.make_bucket, bucket)
|
||||
policy = {
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": {"AWS": ["*"]},
|
||||
"Action": ["s3:GetObject"],
|
||||
"Resource": [f"arn:aws:s3:::{bucket}/*"],
|
||||
}
|
||||
],
|
||||
}
|
||||
await asyncio.to_thread(client.set_bucket_policy, bucket, json.dumps(policy))
|
||||
|
||||
|
||||
async def upload_file_to_minio(
|
||||
file: UploadFile,
|
||||
*,
|
||||
purpose: str,
|
||||
max_size: int,
|
||||
) -> MinioUploadResult:
|
||||
settings = get_settings()
|
||||
if not settings.minio_enabled:
|
||||
raise HTTPException(status_code=503, detail="MinIO 存储未启用")
|
||||
|
||||
total = 0
|
||||
with tempfile.SpooledTemporaryFile(max_size=8 * 1024 * 1024) as tmp:
|
||||
while True:
|
||||
chunk = await file.read(1024 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
total += len(chunk)
|
||||
if total > max_size:
|
||||
raise HTTPException(status_code=400, detail=f"文件大小不能超过 {max_size // 1024 // 1024}MB")
|
||||
tmp.write(chunk)
|
||||
tmp.seek(0)
|
||||
|
||||
client = _client()
|
||||
object_key = _object_key(file.filename or "upload.bin", purpose)
|
||||
await _ensure_bucket(client, settings.minio_bucket)
|
||||
await asyncio.to_thread(
|
||||
client.put_object,
|
||||
settings.minio_bucket,
|
||||
object_key,
|
||||
tmp,
|
||||
total,
|
||||
content_type=file.content_type or "application/octet-stream",
|
||||
metadata={"Cache-Control": "public, max-age=31536000, immutable"},
|
||||
)
|
||||
|
||||
return MinioUploadResult(
|
||||
url=f"{settings.minio_public_url}/{object_key}",
|
||||
object_key=object_key,
|
||||
size=total,
|
||||
)
|
||||
|
||||
|
||||
async def upload_bytes_to_minio(
|
||||
data: bytes,
|
||||
*,
|
||||
object_key: str,
|
||||
content_type: str,
|
||||
) -> MinioUploadResult:
|
||||
settings = get_settings()
|
||||
if not settings.minio_enabled:
|
||||
raise HTTPException(status_code=503, detail="MinIO 存储未启用")
|
||||
|
||||
client = _client()
|
||||
await _ensure_bucket(client, settings.minio_bucket)
|
||||
await asyncio.to_thread(
|
||||
client.put_object,
|
||||
settings.minio_bucket,
|
||||
object_key,
|
||||
io.BytesIO(data),
|
||||
len(data),
|
||||
content_type=content_type,
|
||||
metadata={"Cache-Control": "public, max-age=31536000, immutable"},
|
||||
)
|
||||
return MinioUploadResult(
|
||||
url=f"{settings.minio_public_url}/{object_key}",
|
||||
object_key=object_key,
|
||||
size=len(data),
|
||||
)
|
||||
@@ -5,3 +5,4 @@ python-multipart>=0.0.9
|
||||
PyJWT>=2.8
|
||||
passlib[bcrypt]>=1.7
|
||||
pydantic>=2.7
|
||||
minio>=7.2
|
||||
|
||||
@@ -129,10 +129,11 @@ CITIES = [
|
||||
|
||||
|
||||
MEETUPS = [
|
||||
{"city": "大理", "emoji": "🏯", "date": "2026-07-15", "time": "14:00", "venue": "大理古城人民路咖啡馆", "address": "大理市大理古城人民路中段", "description": "苍山脚下,洱海边,与大理的慢生活游民们一起喝咖啡、晒太阳、聊人生。", "rsvpCount": 12, "gradientFrom": "#3b82f6", "gradientTo": "#4f46e5", "attendees": [{"name": "林晓", "photo": "https://i.pravatar.cc/150?img=1"}, {"name": "张明", "photo": "https://i.pravatar.cc/150?img=2"}], "isUpcoming": True},
|
||||
{"city": "成都", "emoji": "🐼", "date": "2026-07-18", "time": "19:00", "venue": "方所书店", "address": "成都市锦江区中纱帽街8号远洋太古里", "description": "天府之国游民聚会,在文艺书店里品茶聊天,探讨成都的慢生活与远程工作可能性。", "rsvpCount": 9, "gradientFrom": "#22c55e", "gradientTo": "#10b981", "attendees": [{"name": "杨帆", "photo": "https://i.pravatar.cc/150?img=6"}], "isUpcoming": True},
|
||||
{"city": "深圳", "emoji": "🌃", "date": "2026-07-20", "time": "19:30", "venue": "WeWork 卓越前海壹号", "address": "深圳市南山区前海深港合作区卓越前海壹号", "description": "深圳数字游民线下见面会,探讨创业、远程工作与湾区发展机遇。", "rsvpCount": 15, "gradientFrom": "#ff4d4f", "gradientTo": "#ff7a45", "attendees": [{"name": "李娜", "photo": "https://i.pravatar.cc/150?img=10"}], "isUpcoming": True},
|
||||
{"city": "杭州", "emoji": "⛲", "date": "2026-05-25", "time": "18:00", "venue": "梦想小镇·良仓咖啡", "address": "杭州市余杭区梦想小镇互联网村", "description": "西湖畔数字游民聚会,交流产品与运营经验。", "rsvpCount": 11, "gradientFrom": "#06b6d4", "gradientTo": "#0d9488", "attendees": [], "isUpcoming": False},
|
||||
{"city": "线上", "emoji": "💻", "date": "2026-07-09", "time": "20:00", "venue": "MiroTalk 多人视频房", "address": "线上活动", "description": "围绕远程工作的节奏、异地协作和旅居规划做一场开放圆桌。", "rsvpCount": 36, "gradientFrom": "#0f766e", "gradientTo": "#2563eb", "attendees": [{"name": "林晓", "photo": "https://i.pravatar.cc/150?img=1"}, {"name": "张明", "photo": "https://i.pravatar.cc/150?img=2"}], "isUpcoming": True, "mode": "online", "maxAttendees": 80, "organizer": "NomadCNA", "accessLevel": "members", "meetingProvider": "mirotalk", "mirotalkRoom": "nomadcna-remote-work-open-office", "meetingUrl": "", "replayUrl": "", "features": ["group_video", "screen_share", "whiteboard", "host_controls"]},
|
||||
{"city": "大理", "emoji": "🏯", "date": "2026-07-15", "time": "14:00", "venue": "大理古城人民路咖啡馆", "address": "大理市大理古城人民路中段", "description": "苍山脚下,洱海边,与大理的慢生活游民们一起喝咖啡、晒太阳、聊人生。", "rsvpCount": 12, "gradientFrom": "#3b82f6", "gradientTo": "#4f46e5", "attendees": [{"name": "林晓", "photo": "https://i.pravatar.cc/150?img=3"}, {"name": "张明", "photo": "https://i.pravatar.cc/150?img=4"}], "isUpcoming": True, "mode": "hybrid", "maxAttendees": 32, "organizer": "大理城市主理人", "accessLevel": "vip", "meetingProvider": "mirotalk", "mirotalkRoom": "nomadcna-dali-slow-life-roundtable", "meetingUrl": "", "replayUrl": "", "features": ["group_video", "screen_share", "whiteboard", "recording", "host_controls"]},
|
||||
{"city": "成都", "emoji": "🐼", "date": "2026-07-18", "time": "19:00", "venue": "方所书店", "address": "成都市锦江区中纱帽街8号远洋太古里", "description": "天府之国游民聚会,在文艺书店里品茶聊天,探讨成都的慢生活与远程工作可能性。", "rsvpCount": 9, "gradientFrom": "#22c55e", "gradientTo": "#10b981", "attendees": [{"name": "杨帆", "photo": "https://i.pravatar.cc/150?img=6"}], "isUpcoming": True, "mode": "offline", "maxAttendees": 20, "organizer": "成都游民小组", "accessLevel": "public", "meetingProvider": "", "mirotalkRoom": "", "meetingUrl": "", "replayUrl": "", "features": []},
|
||||
{"city": "深圳", "emoji": "🌃", "date": "2026-07-20", "time": "19:30", "venue": "WeWork 卓越前海壹号", "address": "深圳市南山区前海深港合作区卓越前海壹号", "description": "深圳数字游民线下见面会,探讨创业、远程工作与湾区发展机遇。", "rsvpCount": 15, "gradientFrom": "#ff4d4f", "gradientTo": "#ff7a45", "attendees": [{"name": "李娜", "photo": "https://i.pravatar.cc/150?img=10"}], "isUpcoming": True, "mode": "hybrid", "maxAttendees": 48, "organizer": "深圳湾区主理人", "accessLevel": "members", "meetingProvider": "mirotalk", "mirotalkRoom": "nomadcna-shenzhen-maker-night", "meetingUrl": "", "replayUrl": "", "features": ["group_video", "screen_share", "breakout", "host_controls"]},
|
||||
{"city": "杭州", "emoji": "⛲", "date": "2026-05-25", "time": "18:00", "venue": "梦想小镇·良仓咖啡", "address": "杭州市余杭区梦想小镇互联网村", "description": "西湖畔数字游民聚会,交流产品与运营经验。", "rsvpCount": 11, "gradientFrom": "#06b6d4", "gradientTo": "#0d9488", "attendees": [], "isUpcoming": False, "mode": "offline", "maxAttendees": 24, "organizer": "杭州产品游民", "accessLevel": "public", "meetingProvider": "", "mirotalkRoom": "", "meetingUrl": "", "replayUrl": "", "features": []},
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -60,12 +60,12 @@ class Settings:
|
||||
self.pb_admin_email = (
|
||||
os.getenv("PB_ADMIN_EMAIL")
|
||||
or os.getenv("POCKETBASE_EMAIL")
|
||||
or "admin@nomadcna.local"
|
||||
or "xiaoshuang.eric@gmail.com"
|
||||
)
|
||||
self.pb_admin_password = (
|
||||
os.getenv("PB_ADMIN_PASSWORD")
|
||||
or os.getenv("POCKETBASE_PASSWORD")
|
||||
or "NomadCNA123456"
|
||||
or "Xiao4669805@"
|
||||
)
|
||||
self.cookie_name = os.getenv("API_COOKIE_NAME", "pb_session")
|
||||
self.frontend_origins = _split_csv(
|
||||
@@ -75,6 +75,19 @@ class Settings:
|
||||
)
|
||||
)
|
||||
self.upload_dir = Path(os.getenv("UPLOAD_DIR", "backend/uploads"))
|
||||
self.minio_enabled = _bool_env("MINIO_ENABLED", "true")
|
||||
self.minio_endpoint = os.getenv("MINIO_ENDPOINT", "127.0.0.1:9100")
|
||||
self.minio_port = int(os.getenv("MINIO_PORT", "0") or "0")
|
||||
self.minio_use_ssl = _bool_env("MINIO_USE_SSL", "false")
|
||||
self.minio_bucket = os.getenv("MINIO_BUCKET", "hackrobot")
|
||||
self.minio_access_key = os.getenv("MINIO_ACCESS_KEY", "nomadro")
|
||||
self.minio_secret_key = os.getenv("MINIO_SECRET_KEY", "Xiao4669805@")
|
||||
self.minio_region = os.getenv("MINIO_REGION", "china")
|
||||
self.minio_public_url = (
|
||||
os.getenv("MINIO_PUBLIC_URL")
|
||||
or f"https://minio.nomadro.cn/{self.minio_bucket}"
|
||||
).rstrip("/")
|
||||
self.minio_upload_prefix = os.getenv("MINIO_UPLOAD_PREFIX", "joins").strip("/").strip()
|
||||
self.app_env = os.getenv("APP_ENV", "development").strip().lower()
|
||||
self.dev_auto_pay = _bool_env("DEV_AUTO_PAY", "false")
|
||||
self.site_id = os.getenv("SITE_ID") or os.getenv("NEXT_PUBLIC_SITE_ID") or "meetup"
|
||||
@@ -93,7 +106,7 @@ class Settings:
|
||||
or os.getenv("BASE_URL")
|
||||
or self.base_url
|
||||
).rstrip("/")
|
||||
self.email_auth_password = os.getenv("EMAIL_AUTH_PASSWORD", "NomadCNAEmailLogin2026!")
|
||||
self.email_auth_password = os.getenv("EMAIL_AUTH_PASSWORD", "12345678")
|
||||
self.google_client_id = os.getenv("GOOGLE_CLIENT_ID") or os.getenv("NEXT_PUBLIC_GOOGLE_CLIENT_ID") or ""
|
||||
self.payment_provider = os.getenv("PAYMENT_PROVIDER", "zpay").lower()
|
||||
self.payment_default_amount = int(os.getenv("PAYMENT_DEFAULT_AMOUNT", "100") or "100")
|
||||
|
||||
Reference in New Issue
Block a user