Skip to content
Merged
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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,28 @@ This is the backend service for JieNote, built with FastAPI.
uvicorn app.main:app --reload
```

## Redis
- Redis is used for caching and session management.
- Make sure to have Redis installed and running.

```bash
cd path/to/redis
# Start Redis server
redis-server.exe redis.windows.conf
```
Attention!!!
- Make sure the port is not occupied by other services.
- If you want to use the default port, please modify the `redis.windows.conf` file.
- Must connect Redis before running the application. ‼️‼️‼️


## Folder Structure
- `app/`: Contains the main application code.
- `tests/`: Contains test cases.
- `env/`: Virtual environment (not included in version control).



## ER Diagram
![ER Diagram](img/er_diagram.jpg)

Expand Down
32 changes: 32 additions & 0 deletions alembic/versions/2acf0b902f73_添加头像默认值.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""添加头像默认值

Revision ID: 2acf0b902f73
Revises: b7940480e6e6
Create Date: 2025-04-11 22:54:09.734172

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = '2acf0b902f73'
down_revision: Union[str, None] = 'b7940480e6e6'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
34 changes: 34 additions & 0 deletions alembic/versions/9af9d4a35bef_fix_更改password名.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""fix:更改password名

Revision ID: 9af9d4a35bef
Revises: c49010e96150
Create Date: 2025-04-12 10:27:52.832186

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql

# revision identifiers, used by Alembic.
revision: str = '9af9d4a35bef'
down_revision: Union[str, None] = 'c49010e96150'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('password', sa.String(length=60), nullable=False))
op.drop_column('users', 'hash_password')
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('hash_password', mysql.VARCHAR(length=60), nullable=False))
op.drop_column('users', 'password')
# ### end Alembic commands ###
34 changes: 34 additions & 0 deletions alembic/versions/c49010e96150_fix_数据库定义.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""fix 数据库定义

Revision ID: c49010e96150
Revises: 2acf0b902f73
Create Date: 2025-04-12 10:19:29.708681

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql

# revision identifiers, used by Alembic.
revision: str = 'c49010e96150'
down_revision: Union[str, None] = '2acf0b902f73'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('notes', sa.Column('content', sa.String(length=255), nullable=True))
op.drop_column('notes', 'name')
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('notes', sa.Column('name', mysql.VARCHAR(length=30), nullable=True))
op.drop_column('notes', 'content')
# ### end Alembic commands ###
123 changes: 123 additions & 0 deletions app/api/v1/endpoints/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
from fastapi import APIRouter, HTTPException, Depends
from sqlalchemy.orm import Session
from passlib.context import CryptContext
from datetime import datetime, timedelta
import jwt
import smtplib
from email.mime.text import MIMEText
from email.header import Header
import random
import time
import redis
from email.utils import formataddr

from app.db.session import SessionLocal
from app.models.model import User
from app.schemas.auth import UserCreate, UserLogin, UserSendCode
from app.core.config import settings
from app.curd.user import get_user_by_email, create_user

router = APIRouter()

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
SECRET_KEY = settings.SECRET_KEY
ALGORITHM = settings.ALGORITHM
ACCESS_TOKEN_EXPIRE_MINUTES = settings.ACCESS_TOKEN_EXPIRE_MINUTES

# 配置 Redis 连接
while True:
try:
print("Connecting to Redis...")
redis_client = redis.StrictRedis(host='localhost', port=6379, db=0)
redis_client.ping()
break
except redis.ConnectionError:
print("Redis connection failed, retrying...")
time.sleep(1)

def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

def create_access_token(data: dict, expires_delta: timedelta = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt

@router.post("/register", response_model=dict)
def register(user: UserCreate, db: Session = Depends(get_db)):
existing_user = get_user_by_email(db, user.email)
if (redis_client.exists(f"email:{user.email}:code")):
code = redis_client.get(f"email:{user.email}:code").decode("utf-8")
if (user.code != code):
raise HTTPException(status_code=400, detail="Invalid verification code")
else:
raise HTTPException(status_code=400, detail="Verification code expired or not sent")

if (existing_user):
raise HTTPException(status_code=400, detail="Email already registered")
hashed_password = pwd_context.hash(user.password)
create_user(db, user.email, user.username, hashed_password)
return {"msg": "User registered successfully"}

@router.post("/login", response_model=dict)
def login(user: UserLogin, db: Session = Depends(get_db)):
db_user = get_user_by_email(db, user.email)
if not db_user or not pwd_context.verify(user.password, db_user.password):
raise HTTPException(status_code=401, detail="Invalid email or password")
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": db_user.email}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer", }

# 发送验证码
@router.post("/send_code", response_model=dict)
def send_code(user_send_code : UserSendCode, db: Session = Depends(get_db)):
# 检查 Redis 中是否存在该邮箱的发送记录
if redis_client.exists(f"email:{user_send_code.email}:time"):
raise HTTPException(status_code=429, detail="You can only request a verification code once every 5 minutes.")

# 生成随机验证码
code = str(random.randint(100000, 999999))

# SMTP 配置
smtp_server = "smtp.163.com"
smtp_port = 465
sender_email = "19855278313@163.com" # 替换为你的网易邮箱
sender_password = "DHSihwnVc4wS89eV" # 替换为你的授权码

# 邮件内容
subject = "验证码"
body = f"欢迎使用JieNote,很开心遇见您,您的验证码是:{code},请在5分钟内使用。"

# 创建MIMEText对象时需要显式指定子类型和编码
message = MIMEText(_text=body, _subtype='plain', _charset='utf-8')
message["From"] = formataddr(("JieNote团队", "noreply@jienote.com"))
message["To"] = user_send_code.email
message["Subject"] = Header(subject, 'utf-8').encode()
# 添加必要的内容传输编码头
message.add_header('Content-Transfer-Encoding', 'base64')

try:
# 连接 SMTP 服务器并发送邮件
with smtplib.SMTP_SSL(smtp_server, smtp_port) as server:
server.login(sender_email, sender_password)
server.sendmail(sender_email, [user_send_code.email], message.as_string())

# 将验证码和发送时间存储到 Redis,设置 5 分钟过期时间
redis_client.setex(f"email:{user_send_code.email}:code", 300, code)
redis_client.setex(f"email:{user_send_code.email}:time", 300, int(time.time()))

return {"msg": "Verification code sent"}

except smtplib.SMTPException as e:
raise HTTPException(status_code=500, detail=f"Failed to send email: {str(e)}")
10 changes: 8 additions & 2 deletions app/core/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import os
from datetime import timedelta

class Settings:
PROJECT_NAME: str = "JieNote Backend"
VERSION: str = "1.0.0"
PROJECT_NAME: str = "JieNote Backend" # 项目名称
VERSION: str = "1.0.0" # 项目版本
SQLALCHEMY_DATABASE_URL = "mysql+pymysql://root:coders007@47.93.172.156:3306/JieNote" # 替换为实际的用户名、密码和数据库名称
SECRET_KEY: str = os.getenv("SECRET_KEY", "your_secret_key") # 替换为更安全的密钥
ALGORITHM: str = "HS256" # JWT算法
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 # token过期时间

settings = Settings()
12 changes: 12 additions & 0 deletions app/curd/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from sqlalchemy.orm import Session
from app.models.model import User

def get_user_by_email(db: Session, email: str):
return db.query(User).filter(User.email == email).first()

def create_user(db: Session, email: str, username: str,hashed_password: str):
new_user = User(email=email, username=username, password=hashed_password, avatar="app/static/avatar/default.jpg")
db.add(new_user)
db.commit()
db.refresh(new_user)
return new_user
2 changes: 1 addition & 1 deletion app/db/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@

from app.core.config import settings

engine = create_engine(settings.SQLALCHEMY_DATABASE_URI, pool_pre_ping=True) #连接mysql
engine = create_engine(settings.SQLALCHEMY_DATABASE_URL, pool_pre_ping=True) #连接mysql
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
5 changes: 4 additions & 1 deletion app/main.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from fastapi import FastAPI
from app.api.v1.endpoints.auth import router as auth_router

app = FastAPI()

Expand All @@ -8,4 +9,6 @@ def read_root():

@app.get("/items/{item_id}")
def read_item(item_id: int, q: str = None):
return {"item_id": item_id, "q": q}
return {"item_id": item_id, "q": q}

app.include_router(auth_router, prefix="/public", tags=["auth"])
Loading