Commit b0a016d5 authored by Sihan Chen's avatar Sihan Chen
Browse files

secondth commit

parents
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { ApiResponse, RoomMessageGetUpdateRes, Message } from '@/types/api'
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const roomIdStr = searchParams.get('roomId')
const sinceMessageIdStr = searchParams.get('sinceMessageId')
if (!roomIdStr || !sinceMessageIdStr) {
return NextResponse.json({
message: '房间 ID 和消息 ID 不能为空',
code: 1,
data: null
} as ApiResponse, { status: 400 })
}
const roomId = parseInt(roomIdStr)
const sinceMessageId = parseInt(sinceMessageIdStr)
if (isNaN(roomId) || isNaN(sinceMessageId)) {
return NextResponse.json({
message: 'ID 格式错误',
code: 1,
data: null
} as ApiResponse, { status: 400 })
}
// 检查房间是否存在
const room = await prisma.room.findUnique({
where: { id: roomId }
})
if (!room) {
return NextResponse.json({
message: '房间不存在',
code: 1,
data: null
} as ApiResponse, { status: 404 })
}
// 获取指定消息 ID 之后的新消息
const messages = await prisma.message.findMany({
where: {
roomId,
id: {
gt: sinceMessageId
}
},
include: {
sender: {
select: {
username: true
}
}
},
orderBy: {
createdAt: 'asc'
}
})
const formattedMessages: Message[] = messages.map(msg => ({
messageId: msg.id,
roomId: msg.roomId,
sender: msg.sender.username,
content: msg.content,
time: msg.createdAt.getTime()
}))
return NextResponse.json({
message: '获取新消息成功',
code: 0,
data: {
messages: formattedMessages
} as RoomMessageGetUpdateRes
} as ApiResponse)
} catch (error) {
console.error('获取新消息失败:', error)
return NextResponse.json({
message: '获取新消息失败',
code: 1,
data: null
} as ApiResponse, { status: 500 })
}
}
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { ApiResponse, RoomMessageListRes, Message } from '@/types/api'
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const roomIdStr = searchParams.get('roomId')
if (!roomIdStr) {
return NextResponse.json({
message: '房间 ID 不能为空',
code: 1,
data: null
} as ApiResponse, { status: 400 })
}
const roomId = parseInt(roomIdStr)
if (isNaN(roomId)) {
return NextResponse.json({
message: '房间 ID 格式错误',
code: 1,
data: null
} as ApiResponse, { status: 400 })
}
// 检查房间是否存在
const room = await prisma.room.findUnique({
where: { id: roomId }
})
if (!room) {
return NextResponse.json({
message: '房间不存在',
code: 1,
data: null
} as ApiResponse, { status: 404 })
}
// 获取房间消息
const messages = await prisma.message.findMany({
where: { roomId },
include: {
sender: {
select: {
username: true
}
}
},
orderBy: {
createdAt: 'asc'
}
})
const formattedMessages: Message[] = messages.map(msg => ({
messageId: msg.id,
roomId: msg.roomId,
sender: msg.sender.username,
content: msg.content,
time: msg.createdAt.getTime()
}))
return NextResponse.json({
message: '获取消息列表成功',
code: 0,
data: {
messages: formattedMessages
} as RoomMessageListRes
} as ApiResponse)
} catch (error) {
console.error('获取消息列表失败:', error)
return NextResponse.json({
message: '获取消息列表失败',
code: 1,
data: null
} as ApiResponse, { status: 500 })
}
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
color: #333;
background: #f5f5f5;
}
a {
color: inherit;
text-decoration: none;
}
button {
border: none;
outline: none;
cursor: pointer;
font-family: inherit;
}
input {
border: none;
outline: none;
font-family: inherit;
}
/* 聊天室样式 */
.app {
height: 100vh;
display: flex;
flex-direction: column;
}
.auth-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.auth-form {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 400px;
}
.auth-form h1 {
text-align: center;
margin-bottom: 2rem;
color: #333;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
color: #555;
font-weight: 500;
}
.form-group input {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
.form-group input:focus {
border-color: #667eea;
}
.btn {
width: 100%;
padding: 0.75rem;
background: #667eea;
color: white;
border-radius: 4px;
font-size: 1rem;
font-weight: 500;
margin-bottom: 1rem;
transition: background-color 0.2s;
}
.btn:hover {
background: #5a6fd8;
}
.btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.btn-secondary {
background: #6c757d;
}
.btn-secondary:hover {
background: #5a6268;
}
.error-message {
color: #dc3545;
font-size: 0.875rem;
text-align: center;
margin-top: 0.5rem;
}
.auth-switch {
text-align: center;
color: #666;
}
.auth-switch button {
color: #667eea;
background: none;
border: none;
text-decoration: underline;
cursor: pointer;
}
/* 聊天室主界面 */
.chat-container {
display: flex;
height: 100vh;
}
.sidebar {
width: 300px;
background: white;
border-right: 1px solid #e0e0e0;
display: flex;
flex-direction: column;
}
.sidebar-header {
padding: 1rem;
border-bottom: 1px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
}
.sidebar-header h2 {
color: #333;
font-size: 1.2rem;
}
.logout-btn {
background: #dc3545;
color: white;
padding: 0.5rem 1rem;
border-radius: 4px;
font-size: 0.875rem;
}
.logout-btn:hover {
background: #c82333;
}
.room-list {
flex: 1;
overflow-y: auto;
}
.room-item {
padding: 1rem;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: background-color 0.2s;
position: relative;
}
.room-item:hover {
background: #f8f9fa;
}
.room-item.active {
background: #e3f2fd;
border-right: 3px solid #667eea;
}
.room-name {
font-weight: 500;
margin-bottom: 0.25rem;
color: #333;
}
.room-last-message {
font-size: 0.875rem;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.add-room-btn {
margin: 1rem;
background: #667eea;
color: white;
padding: 0.75rem;
border-radius: 4px;
text-align: center;
}
.add-room-btn:hover {
background: #5a6fd8;
}
/* 聊天区域 */
.chat-area {
flex: 1;
display: flex;
flex-direction: column;
background: white;
}
.chat-header {
padding: 1rem;
border-bottom: 1px solid #e0e0e0;
background: white;
}
.chat-header h3 {
color: #333;
}
.no-room-selected {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
color: #666;
font-size: 1.1rem;
}
.messages-container {
flex: 1;
overflow-y: auto;
padding: 1rem;
background: #f9f9f9;
}
.message {
margin-bottom: 1rem;
display: flex;
align-items: flex-start;
}
.message-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: #667eea;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 500;
margin-right: 0.75rem;
flex-shrink: 0;
}
.message-content {
flex: 1;
}
.message-header {
display: flex;
align-items: center;
margin-bottom: 0.25rem;
}
.message-sender {
font-weight: 500;
color: #333;
margin-right: 0.5rem;
}
.message-time {
font-size: 0.75rem;
color: #999;
}
.message-text {
color: #333;
line-height: 1.4;
word-wrap: break-word;
}
.message-input-container {
padding: 1rem;
background: white;
border-top: 1px solid #e0e0e0;
}
.message-input-form {
display: flex;
gap: 0.5rem;
}
.message-input {
flex: 1;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 20px;
font-size: 1rem;
}
.message-input:focus {
border-color: #667eea;
}
.send-btn {
background: #667eea;
color: white;
padding: 0.75rem 1.5rem;
border-radius: 20px;
font-weight: 500;
}
.send-btn:hover:not(:disabled) {
background: #5a6fd8;
}
.send-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
/* 模态框 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal {
background: white;
padding: 2rem;
border-radius: 8px;
width: 100%;
max-width: 400px;
margin: 1rem;
}
.modal h3 {
margin-bottom: 1rem;
color: #333;
}
.modal-buttons {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
}
.modal-buttons .btn {
flex: 1;
margin-bottom: 0;
}
/* 上下文菜单 */
.context-menu {
position: absolute;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
z-index: 1000;
min-width: 120px;
}
.context-menu-item {
padding: 0.5rem 1rem;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
transition: background-color 0.2s;
}
.context-menu-item:last-child {
border-bottom: none;
}
.context-menu-item:hover {
background: #f8f9fa;
}
.context-menu-item.danger {
color: #dc3545;
}
.context-menu-item.danger:hover {
background: #f8d7da;
}
/* 响应式设计 */
@media (max-width: 768px) {
.chat-container {
flex-direction: column;
}
.sidebar {
width: 100%;
max-height: 40vh;
}
.chat-area {
flex: 1;
}
}
import type { Metadata } from 'next'
import { AuthProvider } from '@/contexts/AuthContext'
import './globals.css'
export const metadata: Metadata = {
title: '聊天室 Pro',
description: '一个简易的聊天软件',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh-CN">
<body>
<AuthProvider>
{children}
</AuthProvider>
</body>
</html>
)
}
'use client'
import { useState, useEffect } from 'react'
import { useAuth } from '@/contexts/AuthContext'
import ChatRoom from '@/components/ChatRoom'
export default function Home() {
const { user, nickname, setNickname } = useAuth()
const [currentView, setCurrentView] = useState<'auth' | 'setName' | 'chat'>('auth')
const [isLogin, setIsLogin] = useState(true)
const [formData, setFormData] = useState({
username: '',
password: ''
})
const [nicknameInput, setNicknameInput] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const { login, register } = useAuth()
useEffect(() => {
if (user && nickname) {
setCurrentView('chat')
} else if (user && !nickname) {
setCurrentView('setName')
} else {
setCurrentView('auth')
}
}, [user, nickname])
const handleAuth = async (e: React.FormEvent) => {
e.preventDefault()
if (!formData.username || !formData.password) {
setError('用户名和密码不能为空')
return
}
setLoading(true)
setError('')
try {
if (isLogin) {
await login(formData.username, formData.password)
} else {
await register(formData.username, formData.password)
}
} catch (err) {
setError(err instanceof Error ? err.message : '操作失败')
} finally {
setLoading(false)
}
}
const handleSetNickname = (e: React.FormEvent) => {
e.preventDefault()
if (!nicknameInput.trim()) {
setError('昵称不能为空')
return
}
setNickname(nicknameInput.trim())
}
if (currentView === 'chat') {
return <ChatRoom />
}
if (currentView === 'setName') {
return (
<div className="auth-container">
<form className="auth-form" onSubmit={handleSetNickname}>
<h1>设置昵称</h1>
<div className="form-group">
<label htmlFor="nickname">请输入您的聊天昵称</label>
<input
id="nickname"
type="text"
value={nicknameInput}
onChange={(e) => setNicknameInput(e.target.value)}
placeholder="输入昵称"
maxLength={20}
/>
</div>
{error && <div className="error-message">{error}</div>}
<button type="submit" className="btn">
进入聊天室
</button>
</form>
</div>
)
}
return (
<div className="auth-container">
<form className="auth-form" onSubmit={handleAuth}>
<h1>{isLogin ? '登录' : '注册'}</h1>
<div className="form-group">
<label htmlFor="username">用户名</label>
<input
id="username"
type="text"
value={formData.username}
onChange={(e) => setFormData(prev => ({ ...prev, username: e.target.value }))}
placeholder="请输入用户名"
/>
</div>
<div className="form-group">
<label htmlFor="password">密码</label>
<input
id="password"
type="password"
value={formData.password}
onChange={(e) => setFormData(prev => ({ ...prev, password: e.target.value }))}
placeholder="请输入密码"
/>
</div>
{error && <div className="error-message">{error}</div>}
<button type="submit" className="btn" disabled={loading}>
{loading ? '处理中...' : (isLogin ? '登录' : '注册')}
</button>
<div className="auth-switch">
{isLogin ? '没有账号?' : '已有账号?'}
<button
type="button"
onClick={() => {
setIsLogin(!isLogin)
setError('')
}}
>
{isLogin ? '立即注册' : '立即登录'}
</button>
</div>
</form>
</div>
)
}
'use client'
import { useState, useEffect, useRef } from 'react'
import useSWR, { mutate } from 'swr'
import { useAuth } from '@/contexts/AuthContext'
import { roomApi, messageApi } from '@/lib/api'
import { RoomPreviewInfo, Message } from '@/types/api'
const ChatRoom = () => {
const { user, nickname, logout } = useAuth()
const [selectedRoomId, setSelectedRoomId] = useState<number | null>(null)
const [messageInput, setMessageInput] = useState('')
const [showAddRoomModal, setShowAddRoomModal] = useState(false)
const [newRoomName, setNewRoomName] = useState('')
const [contextMenu, setContextMenu] = useState<{
x: number
y: number
roomId: number
} | null>(null)
const messagesEndRef = useRef<HTMLDivElement>(null)
// 获取房间列表
const { data: roomsData, error: roomsError } = useSWR(
'rooms',
() => roomApi.getRoomList().then(res => res.data)
)
// 获取选中房间的消息
const { data: messagesData } = useSWR(
selectedRoomId ? `messages-${selectedRoomId}` : null,
() => selectedRoomId ? messageApi.getRoomMessages(selectedRoomId).then(res => res.data) : null
)
// 自动滚动到最新消息
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [messagesData?.messages])
// 实时更新消息 - 每秒检查一次
useEffect(() => {
if (!selectedRoomId || !messagesData?.messages.length) return
const interval = setInterval(async () => {
try {
const lastMessageId = messagesData.messages[messagesData.messages.length - 1].messageId
const updateRes = await messageApi.getMessageUpdates(selectedRoomId, lastMessageId)
if (updateRes.data.messages.length > 0) {
// 有新消息,刷新消息列表
mutate(`messages-${selectedRoomId}`)
// 同时刷新房间列表以更新最后一条消息
mutate('rooms')
}
} catch (error) {
console.error('检查消息更新失败:', error)
}
}, 1000)
return () => clearInterval(interval)
}, [selectedRoomId, messagesData?.messages])
// 关闭右键菜单
useEffect(() => {
const handleClick = () => setContextMenu(null)
document.addEventListener('click', handleClick)
return () => document.removeEventListener('click', handleClick)
}, [])
const handleSendMessage = async (e: React.FormEvent) => {
e.preventDefault()
if (!messageInput.trim() || !selectedRoomId || !nickname) return
try {
await messageApi.addMessage({
roomId: selectedRoomId,
content: messageInput.trim(),
sender: nickname
})
setMessageInput('')
// 刷新消息列表和房间列表
mutate(`messages-${selectedRoomId}`)
mutate('rooms')
} catch (error) {
alert('发送消息失败: ' + (error instanceof Error ? error.message : '未知错误'))
}
}
const handleAddRoom = async (e: React.FormEvent) => {
e.preventDefault()
if (!newRoomName.trim() || !nickname) return
try {
await roomApi.addRoom({
user: nickname,
roomName: newRoomName.trim()
})
setNewRoomName('')
setShowAddRoomModal(false)
mutate('rooms')
} catch (error) {
alert('创建房间失败: ' + (error instanceof Error ? error.message : '未知错误'))
}
}
const handleDeleteRoom = async (roomId: number) => {
if (!nickname) return
if (confirm('确定要删除这个房间吗?')) {
try {
await roomApi.deleteRoom({
user: nickname,
roomId
})
if (selectedRoomId === roomId) {
setSelectedRoomId(null)
}
mutate('rooms')
setContextMenu(null)
} catch (error) {
alert('删除房间失败: ' + (error instanceof Error ? error.message : '未知错误'))
}
}
}
const handleRoomRightClick = (e: React.MouseEvent, roomId: number) => {
e.preventDefault()
setContextMenu({
x: e.clientX,
y: e.clientY,
roomId
})
}
const formatTime = (timestamp: number) => {
return new Date(timestamp).toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
})
}
const getAvatarText = (username: string) => {
return username.charAt(0).toUpperCase()
}
const selectedRoom = roomsData?.rooms.find(room => room.roomId === selectedRoomId)
return (
<div className="chat-container">
{/* 侧边栏 - 房间列表 */}
<div className="sidebar">
<div className="sidebar-header">
<h2>聊天室</h2>
<button className="logout-btn" onClick={logout}>
退出
</button>
</div>
<div className="room-list">
{roomsData?.rooms.map((room: RoomPreviewInfo) => (
<div
key={room.roomId}
className={`room-item ${selectedRoomId === room.roomId ? 'active' : ''}`}
onClick={() => setSelectedRoomId(room.roomId)}
onContextMenu={(e) => handleRoomRightClick(e, room.roomId)}
>
<div className="room-name">{room.roomName}</div>
{room.lastMessage && (
<div className="room-last-message">
{room.lastMessage.sender}: {room.lastMessage.content}
</div>
)}
</div>
))}
</div>
<button
className="add-room-btn"
onClick={() => setShowAddRoomModal(true)}
>
+ 添加房间
</button>
</div>
{/* 聊天区域 */}
<div className="chat-area">
{selectedRoom ? (
<>
<div className="chat-header">
<h3>{selectedRoom.roomName}</h3>
</div>
<div className="messages-container">
{messagesData?.messages.map((message: Message) => (
<div key={message.messageId} className="message">
<div className="message-avatar">
{getAvatarText(message.sender)}
</div>
<div className="message-content">
<div className="message-header">
<span className="message-sender">{message.sender}</span>
<span className="message-time">{formatTime(message.time)}</span>
</div>
<div className="message-text">{message.content}</div>
</div>
</div>
))}
<div ref={messagesEndRef} />
</div>
<div className="message-input-container">
<form className="message-input-form" onSubmit={handleSendMessage}>
<input
type="text"
className="message-input"
placeholder="输入消息..."
value={messageInput}
onChange={(e) => setMessageInput(e.target.value)}
/>
<button
type="submit"
className="send-btn"
disabled={!messageInput.trim()}
>
发送
</button>
</form>
</div>
</>
) : (
<div className="no-room-selected">
选择一个房间开始聊天
</div>
)}
</div>
{/* 添加房间模态框 */}
{showAddRoomModal && (
<div className="modal-overlay">
<div className="modal">
<h3>创建新房间</h3>
<form onSubmit={handleAddRoom}>
<div className="form-group">
<label htmlFor="roomName">房间名称</label>
<input
id="roomName"
type="text"
value={newRoomName}
onChange={(e) => setNewRoomName(e.target.value)}
placeholder="请输入房间名称"
maxLength={50}
/>
</div>
<div className="modal-buttons">
<button
type="button"
className="btn btn-secondary"
onClick={() => {
setShowAddRoomModal(false)
setNewRoomName('')
}}
>
取消
</button>
<button type="submit" className="btn">
创建
</button>
</div>
</form>
</div>
</div>
)}
{/* 右键菜单 */}
{contextMenu && (
<div
className="context-menu"
style={{
position: 'fixed',
left: contextMenu.x,
top: contextMenu.y
}}
>
<div
className="context-menu-item danger"
onClick={() => handleDeleteRoom(contextMenu.roomId)}
>
删除房间
</div>
</div>
)}
</div>
)
}
export default ChatRoom
'use client'
import React, { createContext, useContext, useState, useEffect } from 'react'
import { User } from '@/types/api'
import { getToken, setToken, clearToken, authApi } from '@/lib/api'
interface AuthContextType {
user: User | null
nickname: string | null
loading: boolean
login: (username: string, password: string) => Promise<void>
register: (username: string, password: string) => Promise<void>
logout: () => void
setNickname: (nickname: string) => void
}
const AuthContext = createContext<AuthContextType | undefined>(undefined)
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null)
const [nickname, setNicknameState] = useState<string | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
// 检查本地存储的认证状态
const token = getToken()
const storedNickname = localStorage.getItem('nickname')
if (storedNickname) {
setNicknameState(storedNickname)
}
// TODO: 验证 token 有效性
setLoading(false)
}, [])
const login = async (username: string, password: string) => {
try {
const response = await authApi.login({ username, password })
setUser(response.data.user)
setToken(response.data.token)
} catch (error) {
throw error
}
}
const register = async (username: string, password: string) => {
try {
const response = await authApi.register({ username, password })
setUser(response.data.user)
setToken(response.data.token)
} catch (error) {
throw error
}
}
const logout = () => {
setUser(null)
setNicknameState(null)
clearToken()
localStorage.removeItem('nickname')
}
const setNickname = (newNickname: string) => {
setNicknameState(newNickname)
localStorage.setItem('nickname', newNickname)
}
return (
<AuthContext.Provider value={{
user,
nickname,
loading,
login,
register,
logout,
setNickname
}}>
{children}
</AuthContext.Provider>
)
}
export function useAuth() {
const context = useContext(AuthContext)
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider')
}
return context
}
import {
ApiResponse,
LoginRequest,
RegisterRequest,
RoomAddArgs,
RoomDeleteArgs,
MessageAddArgs,
RoomAddRes,
RoomListRes,
RoomMessageListRes,
RoomMessageGetUpdateRes
} from '@/types/api'
const API_BASE = ''
// 获取存储的 token
export function getToken(): string | null {
if (typeof window === 'undefined') return null
return localStorage.getItem('token')
}
// 设置 token
export function setToken(token: string): void {
if (typeof window !== 'undefined') {
localStorage.setItem('token', token)
}
}
// 清除 token
export function clearToken(): void {
if (typeof window !== 'undefined') {
localStorage.removeItem('token')
}
}
// 通用请求函数
async function apiRequest<T>(
endpoint: string,
options: RequestInit = {}
): Promise<ApiResponse<T>> {
const token = getToken()
const headers: HeadersInit = {
'Content-Type': 'application/json',
...(options.headers || {})
}
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
const response = await fetch(`${API_BASE}${endpoint}`, {
...options,
headers
})
const data: ApiResponse<T> = await response.json()
if (data.code !== 0) {
throw new Error(data.message || '请求失败')
}
return data
}
// 认证相关 API
export const authApi = {
async login(credentials: LoginRequest) {
return await apiRequest<{ user: any; token: string }>('/api/auth/login', {
method: 'POST',
body: JSON.stringify(credentials)
})
},
async register(credentials: RegisterRequest) {
return await apiRequest<{ user: any; token: string }>('/api/auth/register', {
method: 'POST',
body: JSON.stringify(credentials)
})
}
}
// 房间相关 API
export const roomApi = {
async addRoom(args: RoomAddArgs) {
return await apiRequest<RoomAddRes>('/api/room/add', {
method: 'POST',
body: JSON.stringify(args)
})
},
async getRoomList() {
return await apiRequest<RoomListRes>('/api/room/list')
},
async deleteRoom(args: RoomDeleteArgs) {
return await apiRequest<null>('/api/room/delete', {
method: 'POST',
body: JSON.stringify(args)
})
}
}
// 消息相关 API
export const messageApi = {
async addMessage(args: MessageAddArgs) {
return await apiRequest<null>('/api/message/add', {
method: 'POST',
body: JSON.stringify(args)
})
},
async getRoomMessages(roomId: number) {
return await apiRequest<RoomMessageListRes>(`/api/room/message/list?roomId=${roomId}`)
},
async getMessageUpdates(roomId: number, sinceMessageId: number) {
return await apiRequest<RoomMessageGetUpdateRes>(
`/api/room/message/getUpdate?roomId=${roomId}&sinceMessageId=${sinceMessageId}`
)
}
}
import bcrypt from 'bcryptjs'
import jwt from 'jsonwebtoken'
import { prisma } from './prisma'
import { User } from '@/types/api'
const JWT_SECRET = process.env.JWT_SECRET || 'your-fallback-secret'
export interface JWTPayload {
userId: number;
username: string;
}
// 密码哈希
export async function hashPassword(password: string): Promise<string> {
return await bcrypt.hash(password, 12)
}
// 验证密码
export async function verifyPassword(password: string, hashedPassword: string): Promise<boolean> {
return await bcrypt.compare(password, hashedPassword)
}
// 生成 JWT Token
export function generateToken(user: { id: number; username: string }): string {
const payload: JWTPayload = {
userId: user.id,
username: user.username
}
return jwt.sign(payload, JWT_SECRET, { expiresIn: '7d' })
}
// 验证 JWT Token
export function verifyToken(token: string): JWTPayload | null {
try {
return jwt.verify(token, JWT_SECRET) as JWTPayload
} catch (error) {
return null
}
}
// 从请求中获取用户信息
export async function getUserFromRequest(request: Request): Promise<User | null> {
const authHeader = request.headers.get('authorization')
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return null
}
const token = authHeader.substring(7)
const payload = verifyToken(token)
if (!payload) {
return null
}
try {
const user = await prisma.user.findUnique({
where: { id: payload.userId },
select: { id: true, username: true }
})
return user
} catch (error) {
return null
}
}
// 注册用户
export async function registerUser(username: string, password: string) {
// 检查用户名是否已存在
const existingUser = await prisma.user.findUnique({
where: { username }
})
if (existingUser) {
throw new Error('用户名已存在')
}
// 创建新用户
const hashedPassword = await hashPassword(password)
const user = await prisma.user.create({
data: {
username,
password: hashedPassword
},
select: {
id: true,
username: true
}
})
return user
}
// 登录用户
export async function loginUser(username: string, password: string) {
const user = await prisma.user.findUnique({
where: { username }
})
if (!user || !(await verifyPassword(password, user.password))) {
throw new Error('用户名或密码错误')
}
return {
id: user.id,
username: user.username
}
}
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
// 基础数据类型
export interface Message {
messageId: number; // 消息 id
roomId: number; // 房间 id
sender: string; // 发送人的 username
content: string; // 消息内容
time: number; // 消息发送时间戳
}
export interface RoomPreviewInfo {
roomId: number;
roomName: string;
// UPDATE 20230814: 这里增加了 null 的可能性
lastMessage: Message | null;
}
// API 请求参数类型
export interface RoomAddArgs {
user: string;
roomName: string;
}
export interface RoomDeleteArgs {
user: string;
roomId: number;
}
export interface MessageAddArgs {
roomId: number;
content: string;
sender: string;
}
export interface RoomMessageListArgs {
roomId: number;
}
export interface RoomMessageGetUpdateArgs {
roomId: number;
sinceMessageId: number;
}
// API 响应类型
export interface ApiResponse<T = any> {
message: string;
code: number;
data: T | null;
}
export interface RoomAddRes {
roomId: number;
}
export interface RoomListRes {
rooms: RoomPreviewInfo[];
}
export interface RoomMessageListRes {
messages: Message[];
}
export interface RoomMessageGetUpdateRes {
messages: Message[];
}
// 用户相关类型
export interface User {
id: number;
username: string;
}
export interface LoginRequest {
username: string;
password: string;
}
export interface RegisterRequest {
username: string;
password: string;
}
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "es6"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment