Commit b5231b65 authored by 何 广一's avatar 何 广一
Browse files

zod check & clear test data

parent ce20f0d3
No preview for this file type
import { NextRequest, NextResponse } from 'next/server';
import * as Ty from '../../../interface'
import * as Db from '../../room-api';
import * as Auth from '../../login-interface';
/*
- url: /api/auth/loggedIn
- method: POST
- Read & Check Cookie
*/
export async function POST(req: NextRequest): Promise<NextResponse<Ty.BackendResponse<Auth.LoggedInResult | null>>> {
const cookieData = await Db.VerifyCookie(req.cookies)
if('code' in cookieData)return NextResponse.json(cookieData);
const argsCheck = Auth.LoggedInArgsSchema.safeParse(await req.json());
if (!argsCheck.success) return Db.IncorrectArgumentResponse(argsCheck.error);
const { user } = argsCheck.data;
const result = await Db.CheckLoggedIn(cookieData.authToken, user);
//invalidUserErrorCode
//reloginErrorCode
//incorrectUserName
const res = NextResponse.json(result);
if (result.code === Auth.invalidUserErrorCode || result.code === Auth.reloginErrorCode) {
res.cookies.set('userToken', '', { maxAge: 0, path: '/' });
res.cookies.set('authToken', '', { maxAge: 0, path: '/' });
}
return res;
}
\ No newline at end of file
import {z} from 'zod';
import * as Ty from '../interface';
export const RoomAddArgsSchema = z.object({
user: z.string().min(2).max(100),
roomName: z.string().min(1).max(100)
});
export const RoomDeleteArgsSchema = z.object({
user: z.string().min(2).max(100),
roomId: z.number()
});
export const maxMessageLength = 1000;
export const MessageAddArgsSchema = z.object({
sender: z.string().min(2).max(100),
roomId: z.number(),
content: z.string().min(1).max(maxMessageLength)
});
export const RoomMessageListArgsSchema = z.object({
roomId: z.coerce.number().int()
});
export const RoomMessageGetUpdateArgsSchema = z.object({
roomId: z.coerce.number().int(),
sinceMessageId: z.coerce.number().int()
});
...@@ -30,6 +30,14 @@ export interface AutoLoginArgs { ...@@ -30,6 +30,14 @@ export interface AutoLoginArgs {
cookie: LoginCookieData; cookie: LoginCookieData;
} }
export interface LoggedInArgs {
user: string;
}
export interface LoggedInResult {
user: string;
}
export const cookieSchema = z.object({ export const cookieSchema = z.object({
userToken: z.string().nullable(), userToken: z.string().nullable(),
authToken: z.string().nullable() authToken: z.string().nullable()
...@@ -46,6 +54,10 @@ export const authArgsSchema = z.object({ ...@@ -46,6 +54,10 @@ export const authArgsSchema = z.object({
userIP: z.string().max(100).nullable() userIP: z.string().max(100).nullable()
}); });
export const LoggedInArgsSchema = z.object({
user: z.string().min(2).max(100)
});
export const reloginErrorCode = 1919810; export const reloginErrorCode = 1919810;
export const invalidUserErrorCode = 8848; export const invalidUserErrorCode = 8848;
export const abnormalLoginState = 108; export const abnormalLoginState = 108;
......
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import * as Ty from '../../../interface' import * as Ty from '../../../interface'
import * as Db from '../../room-api'; import * as Db from '../../room-api';
import { MessageAddArgsSchema } from '../../interface-schema';
/* /*
- url: /api/message/add - url: /api/message/add
...@@ -18,7 +19,10 @@ export async function POST(req: NextRequest): Promise<NextResponse<Ty.BackendRes ...@@ -18,7 +19,10 @@ export async function POST(req: NextRequest): Promise<NextResponse<Ty.BackendRes
const cookieData = await Db.VerifyCookie(req.cookies); const cookieData = await Db.VerifyCookie(req.cookies);
if('code' in cookieData)return NextResponse.json(cookieData); if('code' in cookieData)return NextResponse.json(cookieData);
const { roomId, content, sender } = await req.json() as Ty.MessageAddArgs; const parseResult = MessageAddArgsSchema.safeParse(await req.json());
if(!parseResult.success) return Db.IncorrectArgumentResponse(parseResult.error);
const { roomId, content, sender } = parseResult.data;
const checkResult = await Db.CheckUser(sender, cookieData.userToken); const checkResult = await Db.CheckUser(sender, cookieData.userToken);
if (!checkResult) return Db.IncorrectUserResponse(cookieData.userToken); if (!checkResult) return Db.IncorrectUserResponse(cookieData.userToken);
......
...@@ -4,6 +4,7 @@ import { createHash, randomBytes } from 'crypto'; ...@@ -4,6 +4,7 @@ import { createHash, randomBytes } from 'crypto';
import * as Ty from '../interface' import * as Ty from '../interface'
import {prisma, redis} from "@/lib/db"; import {prisma, redis} from "@/lib/db";
import * as Auth from './login-interface'; import * as Auth from './login-interface';
import {z} from "zod";
export async function MakeError(message: string, code: number = 1, status: number = 500): Promise<NextResponse<Ty.BackendResponse<null>>> { export async function MakeError(message: string, code: number = 1, status: number = 500): Promise<NextResponse<Ty.BackendResponse<null>>> {
return NextResponse.json({ return NextResponse.json({
...@@ -275,6 +276,7 @@ export async function Login(args: Auth.AuthArgs) : Promise<Ty.BackendResponse<Au ...@@ -275,6 +276,7 @@ export async function Login(args: Auth.AuthArgs) : Promise<Ty.BackendResponse<Au
token: authToken, token: authToken,
userName: username, userName: username,
/* 登录过期模拟 : 改成 + 60000 即可模拟1分钟过期 */ /* 登录过期模拟 : 改成 + 60000 即可模拟1分钟过期 */
//expiresAt: new Date(Date.now() + 60000), // 1 minute
expiresAt: new Date(Date.now() + 60 * 60 * 1000 * 24 * 7), // 7 days expiresAt: new Date(Date.now() + 60 * 60 * 1000 * 24 * 7), // 7 days
ua: userAgent, ua: userAgent,
ip: userIP ip: userIP
...@@ -451,3 +453,48 @@ export const IncorrectUserResponse = async (token: string) : Promise<NextRespons ...@@ -451,3 +453,48 @@ export const IncorrectUserResponse = async (token: string) : Promise<NextRespons
data: { user: await getUserNameByToken(token) } data: { user: await getUserNameByToken(token) }
}); });
}; };
export const IncorrectArgumentResponse = async (zerr: z.ZodError) : Promise<NextResponse<Ty.BackendResponse<null>>> => {
console.error(`ZodError: ${zerr}`);
return NextResponse.json({
code: Auth.incorrectUserName,
message: '参数格式错误。: ' + zerr.message,
data: null
}, {status : 400});
};
export const CheckLoggedIn = async (token: string, userName: string) : Promise<Ty.BackendResponse<Auth.LoggedInResult | null>> => {
const authToken = await prisma.authToken.findUnique({
where: { token },
select: { user: true, expiresAt: true }
});
if (!authToken) return {
code: Auth.invalidUserErrorCode,
message: '无效的用户信息,请重新登录。',
data: null
}
if (authToken.expiresAt < new Date()) {
await prisma.authToken.delete({
where: { token }
});
return {
code: Auth.reloginErrorCode,
message: '登录过期,请重新登录。',
data: null
};
}
if (authToken.user.name !== userName) {
return {
code: Auth.incorrectUserName,
message: '用户名与登录令牌不匹配,将重定向到正确用户名。',
data: { user: authToken.user.name }
};
}
return {
code: 0,
message: '用户已登录',
data: { user: authToken.user.name }
};
}
\ No newline at end of file
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import * as Ty from '../../../interface' import * as Ty from '../../../interface'
import * as Db from '../../room-api'; import * as Db from '../../room-api';
import { RoomAddArgsSchema } from '../../interface-schema';
/* /*
- url: /api/room/add - url: /api/room/add
...@@ -20,7 +21,10 @@ export async function POST(req: NextRequest) : Promise<NextResponse<Ty.BackendRe ...@@ -20,7 +21,10 @@ export async function POST(req: NextRequest) : Promise<NextResponse<Ty.BackendRe
const cookieData = await Db.VerifyCookie(req.cookies); const cookieData = await Db.VerifyCookie(req.cookies);
if('code' in cookieData)return NextResponse.json(cookieData); if('code' in cookieData)return NextResponse.json(cookieData);
const { user, roomName }: Ty.RoomAddArgs = await req.json(); const parseResult = RoomAddArgsSchema.safeParse(await req.json());
if(!parseResult.success) return Db.IncorrectArgumentResponse(parseResult.error);
const { user, roomName }: Ty.RoomAddArgs = parseResult.data;
const checkResult = await Db.CheckUser(user, cookieData.userToken); const checkResult = await Db.CheckUser(user, cookieData.userToken);
if (!checkResult) return Db.IncorrectUserResponse(cookieData.userToken); if (!checkResult) return Db.IncorrectUserResponse(cookieData.userToken);
......
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import * as Ty from '../../../interface' import * as Ty from '../../../interface'
import * as Db from '../../room-api'; import * as Db from '../../room-api';
import { RoomDeleteArgsSchema } from '../../interface-schema';
/* /*
- url: /api/room/delete - url: /api/room/delete
...@@ -18,7 +19,10 @@ export async function POST(req: NextRequest): Promise<NextResponse<Ty.BackendRes ...@@ -18,7 +19,10 @@ export async function POST(req: NextRequest): Promise<NextResponse<Ty.BackendRes
const cookieData = await Db.VerifyCookie(req.cookies); const cookieData = await Db.VerifyCookie(req.cookies);
if('code' in cookieData) return NextResponse.json(cookieData); if('code' in cookieData) return NextResponse.json(cookieData);
const { user, roomId } = await req.json() as Ty.RoomDeleteArgs; const parseResult = RoomDeleteArgsSchema.safeParse(await req.json());
if(!parseResult.success) return Db.IncorrectArgumentResponse(parseResult.error);
const { user, roomId } = parseResult.data;
const checkResult = await Db.CheckUser(user, cookieData.userToken); const checkResult = await Db.CheckUser(user, cookieData.userToken);
if (!checkResult) return Db.IncorrectUserResponse(cookieData.userToken); if (!checkResult) return Db.IncorrectUserResponse(cookieData.userToken);
......
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import * as Ty from '../../../../interface' import * as Ty from '../../../../interface'
import * as Db from '../../../room-api'; import * as Db from '../../../room-api';
import { RoomMessageGetUpdateArgsSchema } from '../../../interface-schema';
/* /*
- url: /api/room/message/getUpdate - url: /api/room/message/getUpdate
...@@ -21,10 +22,12 @@ export async function GET(req: NextRequest): Promise<NextResponse<Ty.BackendResp ...@@ -21,10 +22,12 @@ export async function GET(req: NextRequest): Promise<NextResponse<Ty.BackendResp
const cookieData = await Db.VerifyCookie(req.cookies); const cookieData = await Db.VerifyCookie(req.cookies);
if('code' in cookieData) return NextResponse.json(cookieData); if('code' in cookieData) return NextResponse.json(cookieData);
const Params = req.nextUrl.searchParams; const Params: Record<string, string> = {};
const result = await Db.UpdateMessageList({ req.nextUrl.searchParams.forEach((value, key) => {Params[key] = value;});
roomId: Number(Params.get('roomId')), const parseResult = RoomMessageGetUpdateArgsSchema.safeParse(Params);
sinceMessageId: Number(Params.get('sinceMessageId')) if(!parseResult.success) return Db.IncorrectArgumentResponse(parseResult.error);
});
const Args : Ty.RoomMessageGetUpdateArgs = parseResult.data;
const result = await Db.UpdateMessageList(Args);
return NextResponse.json(result); return NextResponse.json(result);
} }
\ No newline at end of file
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import * as Ty from '../../../../interface' import * as Ty from '../../../../interface'
import * as Db from '../../../room-api'; import * as Db from '../../../room-api';
import { RoomMessageListArgsSchema } from '../../../interface-schema';
/* /*
- url: /api/room/message/list - url: /api/room/message/list
...@@ -18,7 +19,13 @@ export async function GET(req: NextRequest): Promise<NextResponse<Ty.BackendResp ...@@ -18,7 +19,13 @@ export async function GET(req: NextRequest): Promise<NextResponse<Ty.BackendResp
const cookieData = await Db.VerifyCookie(req.cookies); const cookieData = await Db.VerifyCookie(req.cookies);
if('code' in cookieData) return NextResponse.json(cookieData); if('code' in cookieData) return NextResponse.json(cookieData);
const Params = req.nextUrl.searchParams; const Params: Record<string, string> = {};
const response = await Db.GetMessageList({ roomId: Number(Params.get('roomId')) }); req.nextUrl.searchParams.forEach((value, key) => {Params[key] = value;});
const parseResult = RoomMessageListArgsSchema.safeParse(Params);
if(!parseResult.success) return Db.IncorrectArgumentResponse(parseResult.error);
const Args : Ty.RoomMessageListArgs = parseResult.data;
const response = await Db.GetMessageList(Args);
return NextResponse.json(response); return NextResponse.json(response);
} }
'use client' 'use client'
import { useEffect } from 'react'; import { useEffect, useState } from 'react';
import { useRoom } from './room-context' import { useRoom } from './room-context'
import { after } from 'node:test'; import { after } from 'node:test';
import { set } from 'zod'; import { set } from 'zod';
...@@ -18,7 +18,10 @@ export default function ConfirmModalProvider() { ...@@ -18,7 +18,10 @@ export default function ConfirmModalProvider() {
errorModalMsg, errorModalMsg,
triggerCreateRoom } = useRoom(); triggerCreateRoom } = useRoom();
const [createPending, setCreatePending] = useState(false);
useEffect(() => { useEffect(() => {
setCreatePending(false);
if (showCreateModal) { if (showCreateModal) {
setNewRoomName(''); setNewRoomName('');
} }
...@@ -30,6 +33,12 @@ export default function ConfirmModalProvider() { ...@@ -30,6 +33,12 @@ export default function ConfirmModalProvider() {
setAfterErrorModalEvent(null); setAfterErrorModalEvent(null);
} }
const handleCreateRoom = (pending : boolean) => {
if(pending)return;
setCreatePending(true);
triggerCreateRoom();
}
return ( return (
<> <>
{showCreateModal && {showCreateModal &&
...@@ -50,14 +59,18 @@ export default function ConfirmModalProvider() { ...@@ -50,14 +59,18 @@ export default function ConfirmModalProvider() {
/> />
<div className="flex justify-end space-x-2"> <div className="flex justify-end space-x-2">
<button <button
className="bg-gray-300 px-4 py-2 rounded" className="bg-gray-200 px-4 py-2 rounded hover:bg-gray-300 active:bg-gray-400"
onClick={() => setShowCreateModal(false)} onClick={() => setShowCreateModal(false)}
> >
取消 取消
</button> </button>
<button <button
className="bg-blue-500 text-white px-4 py-2 rounded" className={
onClick={triggerCreateRoom} createPending ?
"bg-gray-300 text-gray-500 px-4 py-2 rounded cursor-not-allowed" :
"bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 active:bg-blue-700"
}
onClick={() => handleCreateRoom(createPending)}
> >
确定 确定
</button> </button>
......
...@@ -86,7 +86,7 @@ export default function RoomListProvider() { ...@@ -86,7 +86,7 @@ export default function RoomListProvider() {
return roomData.roomId; return roomData.roomId;
} catch (error) { } catch (error) {
console.error('创建房间失败:', error); console.error('创建房间失败:', error);
setRoomsError(error.message); setRoomsError((error as Error).message);
} }
return -1; return -1;
...@@ -176,7 +176,7 @@ export default function RoomListProvider() { ...@@ -176,7 +176,7 @@ export default function RoomListProvider() {
return true; return true;
} catch (error) { } catch (error) {
console.error('删除房间失败:', error); console.error('删除房间失败:', error);
setRoomsError(error.message); setRoomsError((error as Error).message);
} }
return false; return false;
...@@ -234,7 +234,7 @@ export default function RoomListProvider() { ...@@ -234,7 +234,7 @@ export default function RoomListProvider() {
} }
} catch (error) { } catch (error) {
console.error('房间列表获取失败:', error); console.error('房间列表获取失败:', error);
setRoomsError(error.message); setRoomsError((error as Error).message);
} finally { } finally {
setRoomsLoading(false); setRoomsLoading(false);
} }
...@@ -292,7 +292,7 @@ export default function RoomListProvider() { ...@@ -292,7 +292,7 @@ export default function RoomListProvider() {
} }
<h3>{room.roomName}</h3> <h3>{room.roomName}</h3>
{room.lastMessage ? ( {room.lastMessage ? (
<p>{room.lastMessage.sender}: {room.lastMessage.content}</p> <p className="truncate">{room.lastMessage.sender}: {room.lastMessage.content}</p>
) : ( ) : (
<p className="text-gray-500">暂无消息</p> <p className="text-gray-500">暂无消息</p>
)} )}
......
...@@ -4,6 +4,7 @@ import { useRoom } from './room-context' ...@@ -4,6 +4,7 @@ import { useRoom } from './room-context'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import * as Ty from '../interface' import * as Ty from '../interface'
import * as Auth from '../api/login-interface' import * as Auth from '../api/login-interface'
import { maxMessageLength } from '../api/interface-schema'
export default function SendMessageProvider() { export default function SendMessageProvider() {
...@@ -29,7 +30,8 @@ export default function SendMessageProvider() { ...@@ -29,7 +30,8 @@ export default function SendMessageProvider() {
} }
- response: null (只要code为0即为成功) - response: null (只要code为0即为成功)
*/ */
const sendMessage = async () => { const sendMessage = async (allowed : boolean) => {
if (!allowed) return;
if (pending) return; if (pending) return;
setPending(true); setPending(true);
...@@ -79,8 +81,8 @@ export default function SendMessageProvider() { ...@@ -79,8 +81,8 @@ export default function SendMessageProvider() {
} }
throw new Error(`发送失败: ${data.message}`); throw new Error(`发送失败: ${data.message}`);
} }
} catch (error: any) { } catch (error) {
setSendError(error.message); setSendError((error as Error).message);
} finally { } finally {
setPending(false); setPending(false);
} }
...@@ -93,6 +95,11 @@ export default function SendMessageProvider() { ...@@ -93,6 +95,11 @@ export default function SendMessageProvider() {
} }
}, [sendError]); }, [sendError]);
const [allowed, setAllowed] = useState(false);
useEffect(() => {
setAllowed((currentRoomInfo && text.length && text.length <= maxMessageLength && !pending)?true:false);
}, [currentRoomInfo, text, pending]);
return ( return (
<div className="bg-white p-4"> <div className="bg-white p-4">
<textarea <textarea
...@@ -103,13 +110,21 @@ export default function SendMessageProvider() { ...@@ -103,13 +110,21 @@ export default function SendMessageProvider() {
onChange={(e) => setText(e.target.value)} onChange={(e) => setText(e.target.value)}
disabled={!currentRoomInfo} disabled={!currentRoomInfo}
/> />
<div className="text-xs text-gray-500 mt-1 ">
{text.length}/{maxMessageLength}
{
text.length > maxMessageLength && (
<span className="text-red-500 ml-2">超出最大长度</span>
)
}
</div>
<button <button
className={ className={
currentRoomInfo && text.length && !pending ? allowed ?
"bg-blue-500 text-white py-2 px-4 mt-2 w-full hover:bg-blue-600 active:bg-blue-700" : "bg-blue-500 text-white py-2 px-4 mt-2 w-full hover:bg-blue-600 active:bg-blue-700" :
"bg-gray-300 text-gray-500 py-2 px-4 mt-2 w-full cursor-not-allowed" "bg-gray-300 text-gray-500 py-2 px-4 mt-2 w-full cursor-not-allowed"
} }
onClick={sendMessage} onClick={() => sendMessage(allowed)}
> >
发送 发送
</button> </button>
......
...@@ -3,7 +3,9 @@ ...@@ -3,7 +3,9 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { useRoom } from './room-context' import { useRoom } from './room-context'
import { useLocalBackend } from '../interface'; import { useLocalBackend, urlPrefix, BackendResponse } from '../interface';
import * as Auth from '../api/login-interface';
function UsernameCheck_External() { function UsernameCheck_External() {
const { username } = useRoom() const { username } = useRoom()
...@@ -19,8 +21,58 @@ function UsernameCheck_External() { ...@@ -19,8 +21,58 @@ function UsernameCheck_External() {
} }
function UsernameCheck_Local() { function UsernameCheck_Local() {
//URL /api/auth/logged //URL /api/auth/loggedIn
//TODO //invalidUserErrorCode
//reloginErrorCode
//incorrectUserName
const { username, setUsername, setAfterErrorModalEvent, triggerErrorModal } = useRoom();
const checkLogin = async () => {
try{
const Args : Auth.LoggedInArgs = { user: username };
const res = await fetch(`${urlPrefix}/api/auth/loggedIn`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(Args),
});
const data : BackendResponse<Auth.LoggedInResult | null> = await res.json();
if (data.code === Auth.incorrectUserName) {
const result = data.data as Auth.LoggedInResult;
setUsername(result.user);
setAfterErrorModalEvent(() => {
window.location.href = `/chat?username=${encodeURIComponent(result.user)}`;
});
throw new Error(data.message);
}
else if (data.code !== 0) {
setUsername('');
setAfterErrorModalEvent(() => {
window.location.href = `/`;
});
throw new Error(`登录错误: ${data.message}`);
}
} catch (error) {
console.error(error);
triggerErrorModal((error as Error).message);
}
};
//check every 5 minutes
useEffect(() => {
checkLogin();
const interval = setInterval(() => {
checkLogin();
}, 5 * 60 * 1000); // 5 minutes
return () => clearInterval(interval);
}, []);
return null; return null;
} }
......
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