Skip to content
Merged

Dev #69

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ DATABASE_URL=sqlite+aiosqlite:///./data/momshell.db
MODELSCOPE_KEY=your_modelscope_api_key_here
MODELSCOPE_MODEL=Qwen/Qwen2.5-72B-Instruct
# For image generation model (optional)
MODELSCOPE_IMAGE_MODEL=
# MODELSCOPE_IMAGE_MODEL=your_image_model_here

# MediaPipe Configuration
POSE_MODEL_COMPLEXITY=1
Expand All @@ -48,7 +48,7 @@ JWT_REFRESH_TOKEN_EXPIRE_DAYS=7

# Web Search (Firecrawl API for reducing AI hallucinations)
# Get your API key from: https://www.firecrawl.dev/
FIRECRAWL_API_KEY=
# FIRECRAWL_API_KEY=your_firecrawl_api_key_here

# Initial Admin Account (optional, created on first startup)
# Set these to automatically create an admin account when the app starts
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,6 @@ jobs:

- name: TypeScript type check
run: npm run typecheck

- name: Build
run: npm run build
59 changes: 59 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,65 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.5.3] - 2026-02-09

### Added

#### Security Improvements

- **JWT Secret Auto-Generation**: Automatic random JWT secret key generation for Docker deployments
- If `JWT_SECRET_KEY` is not set, a random 64-char hex key is generated at startup
- Startup warning in production mode when using auto-generated (non-persisted) key
- `dev-setup.sh` now auto-generates a persistent JWT secret in `.env`

- **Slider CAPTCHA**: Human verification for login and registration
- New `SliderCaptcha` component with drag-to-verify interaction
- Validates position accuracy, timing, and drag trail
- Required before form submission on both login and register pages

- **Content Moderation Expansion**: Extended sensitive content filtering
- Post titles now moderated (previously only content was checked)
- Comments rejection on moderation failure (previously only marked for review)
- Nickname moderation on registration and profile update
- Post title moderation on edit (previously only content was checked)

#### User Experience Improvements

- **Profile Page Enhancements**:
- Username display in account security section (read-only)
- Email display with edit capability
- Backend support for email updates with duplicate checking

- **Registration Improvements**:
- Role selection during registration (Mom/Dad/Family)
- Backend accepts `role` field in registration request

- **Password Visibility Toggle**: Show/hide password button on login and register pages
- Eye icon toggle for all password fields
- Both password fields share the same visibility state

#### Developer Experience

- **Local Development Setup**:
- New `frontend/.env.example` template with `NEXT_PUBLIC_API_URL`
- `dev-setup.sh` now creates `frontend/.env.local` automatically
- Updated `docs/development.md` with frontend environment setup instructions

### Changed

- **Environment Variables**: Unified `.env.example` style
- Required keys use placeholder values (e.g., `MODELSCOPE_KEY=your_key_here`)
- Optional keys are commented out with placeholder values

### Fixed

- Backend lint errors in `config.py` (unnecessary f-string) and `main.py` (unused import)
- **Moderation error messages not displayed**: Frontend now correctly extracts and shows detailed error messages from backend moderation responses
- Updated `getErrorMessage()` to handle `{message: ...}` response format
- Applied fix to post creation, comments, profile updates, and all error handlers

---

## [0.5.2] - 2026-02-06

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<!-- Badges -->

[![CI](https://img.shields.io/github/actions/workflow/status/koishi510/MomShell/ci.yml?branch=main&style=flat&label=CI)](https://github.com/koishi510/MomShell/actions/workflows/ci.yml)
[![Version](https://img.shields.io/github/v/tag/koishi510/MomShell?style=flat&label=version)](https://github.com/koishi510/MomShell/tags)
[![Version](https://img.shields.io/github/v/tag/koishi510/MomShell?style=flat&label=Version)](https://github.com/koishi510/MomShell/tags)
[![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL%203.0-blue?style=flat)](LICENSE)
[![Python](https://img.shields.io/badge/Python-3.11-3776AB?style=flat&logo=python&logoColor=white)](https://www.python.org/)
[![Node.js](https://img.shields.io/badge/Node.js-22-339933?style=flat&logo=node.js&logoColor=white)](https://nodejs.org/)
Expand Down
19 changes: 18 additions & 1 deletion backend/app/core/config.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
"""Application configuration."""

import secrets
from functools import lru_cache
from pathlib import Path

from pydantic import model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict

# Security constants
DEFAULT_JWT_SECRET = "your-secret-key-change-in-production"
_generated_jwt_secret: str | None = None


def _find_env_file() -> str | None:
"""Find .env file, checking multiple locations for Docker compatibility."""
Expand Down Expand Up @@ -59,11 +65,22 @@ class Settings(BaseSettings):
rest_prompt_interval: int = 300 # seconds

# JWT Authentication
jwt_secret_key: str = "your-secret-key-change-in-production"
jwt_secret_key: str = DEFAULT_JWT_SECRET
jwt_algorithm: str = "HS256"
jwt_access_token_expire_minutes: int = 30
jwt_refresh_token_expire_days: int = 7

@model_validator(mode="after")
def _auto_generate_jwt_secret(self) -> "Settings":
"""Auto-generate JWT secret if using default value."""
global _generated_jwt_secret
if self.jwt_secret_key == DEFAULT_JWT_SECRET:
if _generated_jwt_secret is None:
_generated_jwt_secret = secrets.token_hex(32)
print("[Config] Auto-generated JWT secret (not persisted)")
object.__setattr__(self, "jwt_secret_key", _generated_jwt_secret)
return self

# Web Search (Firecrawl API for reducing AI hallucinations)
firecrawl_api_key: str = ""

Expand Down
17 changes: 17 additions & 0 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,22 @@

settings = get_settings()


def check_security_settings() -> None:
"""Check for insecure settings in production mode."""
from app.core.config import _generated_jwt_secret

if settings.debug:
return # Skip checks in debug mode

if _generated_jwt_secret is not None:
print("\033[93m" + "=" * 60)
print("WARNING: JWT secret was auto-generated (not persisted).")
print("User sessions will be invalidated on restart.")
print("Set JWT_SECRET_KEY in environment for persistent sessions.")
print("=" * 60 + "\033[0m")


# Paths
BASE_DIR = Path(__file__).resolve().parent
STATIC_DIR = BASE_DIR / "static"
Expand Down Expand Up @@ -118,6 +134,7 @@ async def ensure_admin() -> None:
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
"""Application lifespan manager."""
# Startup
check_security_settings()
ensure_db_directory()
await init_db()
# Seed guardian task templates
Expand Down
2 changes: 2 additions & 0 deletions backend/app/services/auth/schemas.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Schemas for authentication."""

from datetime import datetime
from typing import Literal

from pydantic import BaseModel, EmailStr, Field

Expand All @@ -12,6 +13,7 @@ class RegisterRequest(BaseModel):
email: EmailStr
password: str = Field(..., min_length=6, max_length=100)
nickname: str = Field(..., min_length=1, max_length=50)
role: Literal["mom", "dad", "family"] = "mom"


class LoginRequest(BaseModel):
Expand Down
18 changes: 14 additions & 4 deletions backend/app/services/auth/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
from sqlalchemy.orm import selectinload

from app.core.config import get_settings
from app.services.community.enums import CertificationStatus, UserRole
from app.services.community.enums import CertificationStatus, ModerationResult, UserRole
from app.services.community.models import User, UserCertification
from app.services.community.moderation import get_moderation_service

from .schemas import (
LoginRequest,
Expand All @@ -30,15 +31,24 @@ def __init__(self, db: AsyncSession):

async def register(self, request: RegisterRequest) -> UserResponse:
"""Register a new user."""
from fastapi import HTTPException

# Moderate nickname
moderation = get_moderation_service()
nickname_decision = await moderation.moderate_text(request.nickname)
if nickname_decision.result == ModerationResult.REJECTED:
raise HTTPException(
status_code=400,
detail=f"昵称包含敏感内容: {nickname_decision.reason}",
)

# Check if username already exists
existing = await self.db.execute(
select(User).where(
or_(User.username == request.username, User.email == request.email)
)
)
if existing.scalar_one_or_none():
from fastapi import HTTPException

raise HTTPException(status_code=400, detail="用户名或邮箱已存在")

# Create new user
Expand All @@ -47,7 +57,7 @@ async def register(self, request: RegisterRequest) -> UserResponse:
email=request.email,
password_hash=get_password_hash(request.password),
nickname=request.nickname,
role=UserRole.MOM,
role=UserRole(request.role),
is_active=True,
is_banned=False,
)
Expand Down
4 changes: 3 additions & 1 deletion backend/app/services/community/schemas/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from datetime import datetime
from typing import Literal

from pydantic import BaseModel, Field
from pydantic import BaseModel, EmailStr, Field

from ..enums import UserRole
from .base import TagInfo
Expand All @@ -16,6 +16,7 @@ class UserProfileUpdate(BaseModel):
"""Request schema for updating user profile."""

nickname: str | None = Field(None, min_length=1, max_length=50)
email: EmailStr | None = None
avatar_url: str | None = Field(None, max_length=500)
role: FamilyRoleType | None = Field(
None, description="Only family roles allowed: mom, dad, family"
Expand All @@ -36,6 +37,7 @@ class UserProfile(BaseModel):

id: str
nickname: str
email: str
avatar_url: str | None = None
role: UserRole
is_certified: bool = False
Expand Down
Loading
Loading