Commits (2)
# .dockerignore for xlab_chatroom project
# 这个文件告诉Docker在构建镜像时忽略哪些文件和目录
# Docker相关文件
Dockerfile
.dockerignore
docker-compose.yml
nginx.conf
# Node.js相关
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.npm
.yarn
.pnpm-debug.log*
# Next.js相关
.next/
out/
build/
dist/
# 环境变量文件 (敏感信息不应该打包进镜像)
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Git相关
.git
.gitignore
.gitattributes
# IDE和编辑器文件
.vscode/
.idea/
*.swp
*.swo
*~
# 操作系统文件
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# 日志文件
logs
*.log
# 测试相关
coverage/
.nyc_output/
.coverage/
junit.xml
# 临时文件
*.tmp
*.temp
.cache/
# 文档和说明文件 (可选,如果不需要在容器中可以忽略)
README.md
CHANGELOG.md
LICENSE
# 开发相关文件
.eslintrc*
.prettierrc*
.editorconfig
tsconfig.tsbuildinfo
# 数据目录 (如果有的话)
data/
# 备份文件
*.bak
*.backup
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# Dockerfile for xlab_chatroom Next.js App
FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json package-lock.json* ./
RUN npm ci
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
ENV NEXT_TELEMETRY_DISABLED 1
# Build application
RUN npm run build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
CMD ["node", "server.js"]
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
[
{
"userId": 1,
"username": "111",
"email": "gracegrace100118@qq.com",
"passwordHash": "$2b$12$OpXpzChgWCaMzAS0IJIjme7ByXzOHyNMVs.wZu55h2d.gEEFjHgMm",
"createdAt": 1756289824103
}
]
\ No newline at end of file
services:
xlab-chatroom:
build: .
container_name: xlab_chatroom_app
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- JWT_SECRET=your_super_secret_jwt_key_here_change_in_production
volumes:
- ./data:/app/data
\ No newline at end of file
/** @type {import('next').NextConfig} */
const nextConfig = {
// Enable standalone output for Docker deployment
output: "standalone",
// Disable telemetry
experimental: {
// This is needed for standalone output
outputFileTracingRoot: undefined,
},
// Configure images for Docker environment
images: {
unoptimized: true,
},
// Environment variables
env: {
JWT_SECRET: process.env.JWT_SECRET,
},
};
module.exports = nextConfig;
events {
worker_connections 1024;
}
http {
# Upstream for xlab_chatroom app
upstream xlab-chatroom {
server xlab-chatroom:3000;
}
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
server {
listen 80;
server_name localhost;
# Security headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
# Main application routes
location / {
proxy_pass http://xlab-chatroom;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support (for future SSE or WebSocket features)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_cache_bypass $http_upgrade;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# API routes optimization
location /api/ {
proxy_pass http://xlab-chatroom;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# API-specific settings
proxy_buffering off;
proxy_request_buffering off;
}
# Static files caching
location /_next/static/ {
proxy_pass http://xlab-chatroom;
expires 1y;
add_header Cache-Control "public, immutable";
add_header X-Cache-Status "STATIC";
}
# Images and other static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
proxy_pass http://xlab-chatroom;
expires 7d;
add_header Cache-Control "public";
add_header X-Cache-Status "ASSET";
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
}
This diff is collapsed.
{
"name": "xlab_chatroom",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"bcryptjs": "^3.0.2",
"jsonwebtoken": "^9.0.2",
"next": "15.4.6",
"react": "19.1.0",
"react-dom": "19.1.0",
"swr": "^2.3.6"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.6",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"typescript": "^5"
}
}
import { NextRequest, NextResponse } from 'next/server';
import { userStore } from '@/lib/dataStore';
import { verifyPassword, generateToken } from '@/lib/auth';
import { LoginArgs, AuthResponse } from '@/types/auth';
import { ApiResponse } from '@/types/api';
export async function POST(request: NextRequest) {
try {
const body: LoginArgs = await request.json();
const { username, password } = body;
// 验证输入
if (!username || !password) {
return NextResponse.json<ApiResponse>({
code: 1,
message: '用户名和密码不能为空',
});
}
// 查找用户
const user = userStore.findUserByUsername(username);
if (!user) {
return NextResponse.json<ApiResponse>({
code: 1,
message: '用户名或密码错误',
});
}
// 验证密码
const isPasswordValid = await verifyPassword(password, user.passwordHash);
if (!isPasswordValid) {
return NextResponse.json<ApiResponse>({
code: 1,
message: '用户名或密码错误',
});
}
// 生成 token
const token = generateToken({
userId: user.userId,
username: user.username,
});
const response: AuthResponse = {
user: {
userId: user.userId,
username: user.username,
email: user.email,
},
token,
};
return NextResponse.json<ApiResponse<AuthResponse>>({
code: 0,
data: response,
message: '登录成功',
});
} catch (error) {
console.error('登录失败:', error);
return NextResponse.json<ApiResponse>({
code: 1,
message: '服务器错误',
});
}
}
import { NextRequest, NextResponse } from 'next/server';
import { extractUserFromRequest } from '@/lib/auth';
import { userStore } from '@/lib/dataStore';
import { ApiResponse } from '@/types/api';
export async function GET(request: NextRequest) {
try {
const user = extractUserFromRequest(request);
if (!user) {
return NextResponse.json<ApiResponse>({
code: 401,
message: '未授权访问',
}, { status: 401 });
}
// 获取完整用户信息
const fullUser = userStore.findUserById(user.userId);
if (!fullUser) {
return NextResponse.json<ApiResponse>({
code: 404,
message: '用户不存在',
}, { status: 404 });
}
return NextResponse.json<ApiResponse>({
code: 200,
data: {
userId: fullUser.userId,
username: fullUser.username,
email: fullUser.email,
},
message: '获取用户信息成功',
});
} catch (error) {
console.error('获取用户信息失败:', error);
return NextResponse.json<ApiResponse>({
code: 500,
message: '服务器错误',
}, { status: 500 });
}
}
import { NextRequest, NextResponse } from 'next/server';
import { userStore } from '@/lib/dataStore';
import { hashPassword, generateToken } from '@/lib/auth';
import { RegisterArgs, AuthResponse } from '@/types/auth';
import { ApiResponse } from '@/types/api';
export async function POST(request: NextRequest) {
try {
const body: RegisterArgs = await request.json();
const { username, password, email } = body;
// 验证输入
if (!username || !password) {
return NextResponse.json<ApiResponse>({
code: 1,
message: '用户名和密码不能为空',
});
}
if (username.length < 3 || password.length < 6) {
return NextResponse.json<ApiResponse>({
code: 1,
message: '用户名至少3位,密码至少6位',
});
}
// 检查用户名是否已存在
if (userStore.isUsernameExists(username)) {
return NextResponse.json<ApiResponse>({
code: 1,
message: '用户名已存在',
});
}
// 创建用户
const passwordHash = await hashPassword(password);
const user = await userStore.createUser(username, email, passwordHash);
// 生成 token
const token = generateToken({
userId: user.userId,
username: user.username,
});
const response: AuthResponse = {
user: {
userId: user.userId,
username: user.username,
email: user.email,
},
token,
};
return NextResponse.json<ApiResponse<AuthResponse>>({
code: 0,
data: response,
message: '注册成功',
});
} catch (error) {
console.error('注册失败:', error);
return NextResponse.json<ApiResponse>({
code: 1,
message: '服务器错误',
});
}
}
import { NextResponse } from "next/server";
import { extractUserFromRequest } from "@/lib/auth";
import { MessageAddArgs } from "@/types/api";
import { Message } from "@/types";
import { getNextMessageId, addMessage } from "@/lib/dataStore";
export async function POST(request: Request) {
try {
// 验证用户身份
const user = extractUserFromRequest(request);
if (!user) {
return NextResponse.json({
code: 1,
message: "请先登录",
});
}
const body = await request.json();
const { roomId, content } = body;
console.log("收到发送消息请求:", { roomId, content, user: user.username });
if (!roomId || !content) {
return NextResponse.json({
code: 1,
message: "房间ID和消息内容不能为空",
});
}
// 创建新消息 - 使用认证用户的用户名
const newMessage: Message = {
messageId: getNextMessageId(),
roomId: roomId,
sender: user.username, // 使用认证用户的用户名
content: content.trim(),
time: Date.now(),
};
console.log("创建新消息:", newMessage);
// 添加消息到共享存储
addMessage(newMessage);
return NextResponse.json({
code: 0,
message: "消息发送成功",
});
} catch (error) {
console.error("发送消息失败:", error);
return NextResponse.json({
code: 1,
message: "发送消息失败",
});
}
}
import { NextResponse } from "next/server";
import { extractUserFromRequest } from "@/lib/auth";
import { RoomAddArgs, RoomAddRes } from "@/types/api";
import { Message } from "@/types";
import {
getNextRoomId,
getNextMessageId,
addRoom,
addMessage,
rooms,
} from "@/lib/dataStore";
export async function POST(request: Request) {
try {
// 验证用户身份
const user = extractUserFromRequest(request);
if (!user) {
return NextResponse.json({
code: 1,
message: "请先登录",
});
}
const body = await request.json();
const { roomName } = body;
console.log("创建房间请求:", {
user: user.username,
roomName,
currentRoomsCount: rooms.length,
});
if (!roomName) {
console.log("创建房间参数验证失败: roomName 为空");
return NextResponse.json({
code: 1,
message: "房间名称不能为空",
});
}
const newRoomId = getNextRoomId();
console.log("生成新房间ID:", newRoomId);
// 创建欢迎消息
const welcomeMessage: Message = {
messageId: getNextMessageId(),
roomId: newRoomId,
sender: "系统",
content: `欢迎来到${roomName}房间!`,
time: Date.now(),
};
console.log("创建欢迎消息:", welcomeMessage);
// 创建新房间 - 使用你原有的接口格式
const newRoom = {
roomId: newRoomId,
roomName: roomName.trim(),
lastMessage: welcomeMessage,
};
console.log("准备添加房间:", newRoom);
// 添加房间和欢迎消息
addRoom(newRoom);
addMessage(welcomeMessage);
console.log("房间和消息添加完成");
const response: RoomAddRes = { roomId: newRoomId };
console.log("创建房间成功:", response);
return NextResponse.json({
code: 0,
data: response,
});
} catch (error) {
console.error("创建房间API异常:", error);
return NextResponse.json({
code: 1,
message:
"创建房间失败: " +
(error instanceof Error ? error.message : String(error)),
});
}
}
import { NextResponse } from "next/server";
import { extractUserFromRequest } from "@/lib/auth";
import { RoomDeleteArgs } from "@/types/api";
import { rooms, removeRoom } from "@/lib/dataStore";
export async function POST(request: Request) {
try {
// 验证用户身份
const user = extractUserFromRequest(request);
if (!user) {
return NextResponse.json({
code: 1,
message: "请先登录",
});
}
const body = await request.json();
const { roomId } = body;
console.log("删除房间请求:", {
user: user.username,
roomId,
currentRoomsCount: rooms.length,
});
if (roomId === undefined || roomId === null) {
console.log("参数验证失败: roomId 为空");
return NextResponse.json({
code: 1,
message: "房间ID不能为空",
});
}
// 检查是否至少保留一个房间(提前检查)
if (rooms.length <= 1) {
console.log("房间数量不足,无法删除");
return NextResponse.json({
code: 1,
message: "至少需要保留一个房间",
});
}
// 确保 roomId 是数字类型
const roomIdToDelete = Number(roomId);
if (isNaN(roomIdToDelete)) {
console.log("房间ID格式错误:", roomId);
return NextResponse.json({
code: 1,
message: "房间ID格式错误",
});
}
// 检查房间是否存在
const roomExists = rooms.some((room) => room.roomId === roomIdToDelete);
console.log("房间存在性检查:", {
roomIdToDelete,
exists: roomExists,
});
if (!roomExists) {
console.log(`房间 ${roomIdToDelete} 不存在,视为已删除`);
return NextResponse.json({
code: 0,
message: "房间已删除或不存在",
});
}
console.log(`开始删除房间 ${roomIdToDelete}`);
const success = removeRoom(roomIdToDelete);
console.log("删除结果:", {
success,
remainingRoomsCount: rooms.length,
});
if (!success) {
console.log("删除房间操作失败");
return NextResponse.json({
code: 1,
message: "删除房间失败",
});
}
console.log(`房间 ${roomIdToDelete} 删除成功`);
return NextResponse.json({
code: 0,
message: "删除成功",
});
} catch (error) {
console.error("删除房间API异常:", error);
return NextResponse.json({
code: 1,
message:
"删除房间失败: " +
(error instanceof Error ? error.message : String(error)),
});
}
}
// 文件位置:src/app/api/room/list/route.ts
import { NextResponse } from 'next/server';
import { RoomListRes } from '@/types/api';
import { rooms } from '@/lib/dataStore';
export async function GET() {
try {
const response: RoomListRes = { rooms };
return NextResponse.json({
code: 0,
data: response,
});
} catch (error) {
return NextResponse.json({
code: 1,
message: '获取房间列表失败'
});
}
}
// 文件位置:src/app/api/room/message/getUpdate/route.ts
import { NextResponse } from 'next/server';
import { RoomMessageGetUpdateRes } from '@/types/api';
import { getRoomUpdateMessages } from '@/lib/dataStore';
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const roomId = parseInt(searchParams.get('roomId') || '0');
const sinceMessageId = parseInt(searchParams.get('sinceMessageId') || '0');
if (!roomId) {
return NextResponse.json({
code: 1,
message: '房间ID必须提供'
});
}
// 获取指定房间中ID大于sinceMessageId的消息
const newMessages = getRoomUpdateMessages(roomId, sinceMessageId);
const response: RoomMessageGetUpdateRes = { messages: newMessages };
return NextResponse.json({
code: 0,
data: response,
});
} catch (error) {
return NextResponse.json({
code: 1,
message: '获取消息更新失败'
});
}
}
// 文件位置:src/app/api/room/message/list/route.ts
import { NextResponse } from 'next/server';
import { RoomMessageListRes } from '@/types/api';
import { getRoomMessages } from '@/lib/dataStore';
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const roomId = parseInt(searchParams.get('roomId') || '0');
if (!roomId) {
return NextResponse.json({
code: 1,
message: '房间ID必须提供'
});
}
const roomMessages = getRoomMessages(roomId);
const response: RoomMessageListRes = { messages: roomMessages };
return NextResponse.json({
code: 0,
data: response,
});
} catch (error) {
return NextResponse.json({
code: 1,
message: '获取消息列表失败'
});
}
}
"use client";
import React from "react";
import ProtectedRoute from "@/components/Auth/ProtectedRoute";
import UserInfo from "@/components/UserInfo/UserInfo";
import ChatRoom from "@/components/ChatRoom/ChatRoom";
export default function ChatRoomPage() {
return (
<ProtectedRoute>
<div
className="app-container"
style={{ minHeight: "100vh", background: "#f8f9fa" }}
>
<UserInfo />
<ChatRoom />
</div>
</ProtectedRoute>
);
}