update
This commit is contained in:
@@ -14,3 +14,12 @@ mysqld --basedir=/usr \
|
||||
--socket=/home/aecw/re-AstarCupWeb/database/mysql.sock \
|
||||
--pid-file=/home/aecw/re-AstarCupWeb/database/mysql.pid &
|
||||
``
|
||||
|
||||
# env
|
||||
``
|
||||
OSU_CLIENT_ID =
|
||||
OSU_CLIENT_SECRET =
|
||||
OSU_REDIRECT_URI =
|
||||
DATABASE_URL=""
|
||||
DATABASE_CLIENT_URL=""
|
||||
``
|
||||
88
app/api/auth/callback/osu/route.ts
Normal file
88
app/api/auth/callback/osu/route.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getOsuToken, getMyOsuInfo } from '@/app/lib/osuAuth';
|
||||
import { CreateUser } from '@/app/lib/UserOperation';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const code = searchParams.get('code');
|
||||
const error = searchParams.get('error');
|
||||
const errorDescription = searchParams.get('error_description');
|
||||
|
||||
// 检查错误
|
||||
if (error) {
|
||||
console.error('OAuth error:', error, errorDescription);
|
||||
return NextResponse.redirect(new URL('/debug?error=' + encodeURIComponent(errorDescription || error), request.url));
|
||||
}
|
||||
|
||||
// 检查授权码
|
||||
if (!code) {
|
||||
console.error('No authorization code provided');
|
||||
return NextResponse.redirect(new URL('/debug?error=No authorization code provided', request.url));
|
||||
}
|
||||
|
||||
console.log('Received OAuth code:', code);
|
||||
|
||||
try {
|
||||
// 获取访问令牌
|
||||
const tokenData = await getOsuToken(code);
|
||||
console.log('Token obtained successfully');
|
||||
|
||||
// 获取用户信息
|
||||
const userInfo = await getMyOsuInfo(tokenData.access_token);
|
||||
console.log('User info obtained:', userInfo.username, userInfo.id);
|
||||
|
||||
// 创建更新用户
|
||||
const userSession = await CreateUser({
|
||||
osuid: userInfo.id,
|
||||
username: userInfo.username,
|
||||
avatar_url: userInfo.avatar_url,
|
||||
cover_url: userInfo.cover?.url,
|
||||
country_code: userInfo.country_code,
|
||||
pp: userInfo.statistics?.pp || 0,
|
||||
global_rank: userInfo.statistics?.global_rank || null,
|
||||
country_rank: userInfo.statistics?.country_rank || null,
|
||||
});
|
||||
|
||||
console.log('User created/updated successfully:', userSession.username);
|
||||
|
||||
const redirectUrl = new URL('/debug', request.url);
|
||||
redirectUrl.searchParams.set('success', 'true');
|
||||
redirectUrl.searchParams.set('username', userSession.username);
|
||||
redirectUrl.searchParams.set('osuid', userSession.osuid.toString());
|
||||
redirectUrl.searchParams.set('userId', userSession.id.toString());
|
||||
|
||||
const response = NextResponse.redirect(redirectUrl);
|
||||
const userCookieData = {
|
||||
id: userSession.id,
|
||||
osuid: userSession.osuid,
|
||||
username: userSession.username,
|
||||
avatar_url: userSession.avatar_url || '',
|
||||
country_code: userSession.country_code,
|
||||
};
|
||||
|
||||
// 设置 cookie 7 天
|
||||
response.cookies.set('user_session', JSON.stringify(userCookieData), {
|
||||
httpOnly: process.env.NODE_ENV === 'production',
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 604800, // 7 天
|
||||
path: '/',
|
||||
});
|
||||
|
||||
return response;
|
||||
|
||||
} catch (apiError) {
|
||||
console.error('API error during OAuth flow:', apiError);
|
||||
return NextResponse.redirect(
|
||||
new URL('/debug?error=' + encodeURIComponent('Failed to process OAuth: ' + (apiError instanceof Error ? apiError.message : String(apiError))), request.url)
|
||||
);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Unexpected error in OAuth callback:', error);
|
||||
return NextResponse.redirect(
|
||||
new URL('/debug?error=' + encodeURIComponent('Unexpected error: ' + (error instanceof Error ? error.message : String(error))), request.url)
|
||||
);
|
||||
}
|
||||
}
|
||||
15
app/api/auth/getAuthUrl/route.ts
Normal file
15
app/api/auth/getAuthUrl/route.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getOsuAuthUrl } from '@/app/lib/osuAuth';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const authUrl = getOsuAuthUrl();
|
||||
return NextResponse.redirect(authUrl);
|
||||
} catch (error) {
|
||||
console.error('Error generating OAuth URL:', error);
|
||||
return NextResponse.redirect(
|
||||
new URL('/debug?error=' + encodeURIComponent('Failed to initialize OAuth: ' + (error instanceof Error ? error.message : String(error))),
|
||||
new URL('/', process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'))
|
||||
);
|
||||
}
|
||||
}
|
||||
76
app/api/user/me/route.ts
Normal file
76
app/api/user/me/route.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getUserByOsuId } from '@/app/lib/UserOperation';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// 从 cookie 中获取用户会话
|
||||
const userSessionCookie = request.cookies.get('user_session');
|
||||
|
||||
console.log('Cookie received:', userSessionCookie ? 'yes' : 'no');
|
||||
if (userSessionCookie) {
|
||||
console.log('Cookie value length:', userSessionCookie.value.length);
|
||||
console.log('Cookie value first 100 chars:', userSessionCookie.value.substring(0, 100));
|
||||
}
|
||||
|
||||
if (!userSessionCookie || !userSessionCookie.value) {
|
||||
console.log('No user session cookie found');
|
||||
return NextResponse.json(
|
||||
{ error: 'Not authenticated' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const userData = JSON.parse(userSessionCookie.value);
|
||||
console.log('Parsed user data:', userData);
|
||||
|
||||
// 从数据库获取完整的用户信息
|
||||
const user = await getUserByOsuId(userData.osuid);
|
||||
|
||||
if (!user) {
|
||||
console.log('User not found in database for osuid:', userData.osuid);
|
||||
// 如果数据库中没有用户,清除 cookie
|
||||
const response = NextResponse.json(
|
||||
{ error: 'User not found in database' },
|
||||
{ status: 404 }
|
||||
);
|
||||
response.cookies.delete('user_session');
|
||||
return response;
|
||||
}
|
||||
|
||||
console.log('User found:', user.username);
|
||||
return NextResponse.json(user);
|
||||
} catch (parseError) {
|
||||
console.error('Error parsing user session cookie:', parseError);
|
||||
console.error('Raw cookie value:', userSessionCookie.value);
|
||||
// 清除无效的 cookie
|
||||
const response = NextResponse.json(
|
||||
{ error: 'Invalid session' },
|
||||
{ status: 400 }
|
||||
);
|
||||
response.cookies.delete('user_session');
|
||||
return response;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting current user:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
// 清除用户会话 cookie
|
||||
const response = NextResponse.json({ success: true });
|
||||
response.cookies.delete('user_session');
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Error logging out:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
51
app/api/user/search/route.ts
Normal file
51
app/api/user/search/route.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getUserByOsuId, getUserByUsername } from '@/app/lib/UserOperation';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const type = searchParams.get('type'); // 'osuid' or 'username'
|
||||
const value = searchParams.get('value');
|
||||
|
||||
if (!type || !value) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing type or value parameter' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
let user;
|
||||
if (type === 'osuid') {
|
||||
const osuid = parseInt(value);
|
||||
if (isNaN(osuid)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid osuid' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
user = await getUserByOsuId(osuid);
|
||||
} else if (type === 'username') {
|
||||
user = await getUserByUsername(value);
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid type parameter. Use "osuid" or "username"' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ error: 'User not found' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(user);
|
||||
} catch (error) {
|
||||
console.error('Error searching user:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
25
app/api/user/update/route.ts
Normal file
25
app/api/user/update/route.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { updateProfile } from '@/app/lib/UserOperation';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { osuid, ...updateData } = body;
|
||||
|
||||
if (!osuid) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing osuid parameter' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const updatedUser = await updateProfile(osuid, updateData);
|
||||
return NextResponse.json(updatedUser);
|
||||
} catch (error) {
|
||||
console.error('Error updating user:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { prisma } from '@/app/lib/PrismaClient';
|
||||
|
||||
// GET: 获取配置
|
||||
export async function GET() {
|
||||
try {
|
||||
const config = await prisma.tournamentConfig.findUnique({
|
||||
where: { id: 1 },
|
||||
});
|
||||
|
||||
return NextResponse.json(config);
|
||||
} catch (error) {
|
||||
console.error('获取比赛配置失败:', error);
|
||||
return NextResponse.json(
|
||||
{ error: '获取配置失败' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
473
app/debug/components/RegisterUserCard.tsx
Normal file
473
app/debug/components/RegisterUserCard.tsx
Normal file
@@ -0,0 +1,473 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Season } from '@/app/generated/prisma/enums';
|
||||
|
||||
interface UserInfo {
|
||||
id: number;
|
||||
osuid: number;
|
||||
username: string;
|
||||
avatar_url?: string;
|
||||
cover_url?: string;
|
||||
country_code: string;
|
||||
pp: number;
|
||||
global_rank: number;
|
||||
country_rank: number;
|
||||
seasonal: Season;
|
||||
userState: string;
|
||||
approved: boolean;
|
||||
seed: number;
|
||||
}
|
||||
|
||||
export default function RegisterUserCard() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
|
||||
const [searchType, setSearchType] = useState<'osuid' | 'username'>('osuid');
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [updateData, setUpdateData] = useState({
|
||||
username: '',
|
||||
avatar_url: '',
|
||||
cover_url: '',
|
||||
country_code: '',
|
||||
pp: 0,
|
||||
global_rank: 0,
|
||||
country_rank: 0,
|
||||
});
|
||||
|
||||
// 检查URL参数,处理OAuth回调结果,并检查当前登录状态
|
||||
useEffect(() => {
|
||||
const checkCurrentUser = async () => {
|
||||
console.log('Checking current user session...');
|
||||
try {
|
||||
const response = await fetch('/api/user/me', {
|
||||
credentials: 'include', // 确保发送 cookie
|
||||
});
|
||||
|
||||
console.log('API response status:', response.status);
|
||||
|
||||
if (response.ok) {
|
||||
const user = await response.json();
|
||||
console.log('User found:', user.username);
|
||||
|
||||
setUserInfo({
|
||||
id: user.id,
|
||||
osuid: user.osuid,
|
||||
username: user.username,
|
||||
avatar_url: user.avatar_url,
|
||||
cover_url: user.cover_url,
|
||||
country_code: user.country_code,
|
||||
pp: user.pp,
|
||||
global_rank: user.global_rank,
|
||||
country_rank: user.country_rank,
|
||||
seasonal: user.seasonal,
|
||||
userState: user.userState,
|
||||
approved: user.approved === 1,
|
||||
seed: user.seed,
|
||||
});
|
||||
|
||||
setUpdateData({
|
||||
username: user.username,
|
||||
avatar_url: user.avatar_url || '',
|
||||
cover_url: user.cover_url || '',
|
||||
country_code: user.country_code,
|
||||
pp: user.pp,
|
||||
global_rank: user.global_rank,
|
||||
country_rank: user.country_rank,
|
||||
});
|
||||
} else {
|
||||
console.log('User not authenticated or API error:', response.status);
|
||||
}
|
||||
} catch (err) {
|
||||
// 忽略错误,用户可能未登录
|
||||
console.log('Error checking user session:', err);
|
||||
}
|
||||
|
||||
// 检查URL参数,处理OAuth回调结果
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const successParam = params.get('success');
|
||||
const usernameParam = params.get('username');
|
||||
const osuidParam = params.get('osuid');
|
||||
const errorParam = params.get('error');
|
||||
|
||||
if (successParam === 'true' && usernameParam && osuidParam) {
|
||||
setSuccess(`用户 ${usernameParam} (osuid: ${osuidParam}) 注册/登录成功!`);
|
||||
// 清空URL参数
|
||||
const newUrl = window.location.pathname;
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
}
|
||||
|
||||
if (errorParam) {
|
||||
setError(decodeURIComponent(errorParam));
|
||||
// 清空URL参数
|
||||
const newUrl = window.location.pathname;
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
}
|
||||
};
|
||||
|
||||
checkCurrentUser();
|
||||
}, []);
|
||||
|
||||
const handleOsuLogin = () => {
|
||||
window.location.href = '/api/auth/getAuthUrl';
|
||||
};
|
||||
|
||||
const handleSearchUser = async () => {
|
||||
if (!searchValue.trim()) {
|
||||
setError('请输入搜索值');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/user/search?type=${searchType}&value=${encodeURIComponent(searchValue)}`);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || `搜索失败: ${response.status}`);
|
||||
}
|
||||
|
||||
const user = await response.json();
|
||||
|
||||
if (user) {
|
||||
setUserInfo({
|
||||
id: user.id,
|
||||
osuid: user.osuid,
|
||||
username: user.username,
|
||||
avatar_url: user.avatar_url,
|
||||
cover_url: user.cover_url,
|
||||
country_code: user.country_code,
|
||||
pp: user.pp,
|
||||
global_rank: user.global_rank,
|
||||
country_rank: user.country_rank,
|
||||
seasonal: user.seasonal,
|
||||
userState: user.userState,
|
||||
approved: user.approved === 1,
|
||||
seed: user.seed,
|
||||
});
|
||||
|
||||
setUpdateData({
|
||||
username: user.username,
|
||||
avatar_url: user.avatar_url || '',
|
||||
cover_url: user.cover_url || '',
|
||||
country_code: user.country_code,
|
||||
pp: user.pp,
|
||||
global_rank: user.global_rank,
|
||||
country_rank: user.country_rank,
|
||||
});
|
||||
|
||||
setSuccess(`找到用户: ${user.username} (ID: ${user.id})`);
|
||||
} else {
|
||||
setError('未找到用户');
|
||||
setUserInfo(null);
|
||||
}
|
||||
} catch (err) {
|
||||
setError('搜索用户失败: ' + (err instanceof Error ? err.message : String(err)));
|
||||
setUserInfo(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateProfile = async () => {
|
||||
if (!userInfo) {
|
||||
setError('没有用户信息可更新');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const updatePayload: {
|
||||
username?: string;
|
||||
avatar_url?: string;
|
||||
cover_url?: string;
|
||||
country_code?: string;
|
||||
pp?: number;
|
||||
global_rank?: number;
|
||||
country_rank?: number;
|
||||
} = {};
|
||||
|
||||
// 只添加有变化的字段
|
||||
if (updateData.username !== userInfo.username) updatePayload.username = updateData.username;
|
||||
if (updateData.avatar_url !== userInfo.avatar_url) updatePayload.avatar_url = updateData.avatar_url;
|
||||
if (updateData.cover_url !== userInfo.cover_url) updatePayload.cover_url = updateData.cover_url;
|
||||
if (updateData.country_code !== userInfo.country_code) updatePayload.country_code = updateData.country_code;
|
||||
if (updateData.pp !== userInfo.pp) updatePayload.pp = updateData.pp;
|
||||
if (updateData.global_rank !== userInfo.global_rank) updatePayload.global_rank = updateData.global_rank;
|
||||
if (updateData.country_rank !== userInfo.country_rank) updatePayload.country_rank = updateData.country_rank;
|
||||
|
||||
// 如果没有变化,直接返回
|
||||
if (Object.keys(updatePayload).length === 0) {
|
||||
setSuccess('没有需要更新的信息');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/user/update', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
osuid: userInfo.osuid,
|
||||
...updatePayload,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || `更新失败: ${response.status}`);
|
||||
}
|
||||
|
||||
const updatedUser = await response.json();
|
||||
|
||||
setUserInfo({
|
||||
id: updatedUser.id,
|
||||
osuid: updatedUser.osuid,
|
||||
username: updatedUser.username,
|
||||
avatar_url: updatedUser.avatar_url,
|
||||
cover_url: updatedUser.cover_url,
|
||||
country_code: updatedUser.country_code,
|
||||
pp: updatedUser.pp,
|
||||
global_rank: updatedUser.global_rank,
|
||||
country_rank: updatedUser.country_rank,
|
||||
seasonal: updatedUser.seasonal,
|
||||
userState: updatedUser.userState,
|
||||
approved: updatedUser.approved === 1,
|
||||
seed: updatedUser.seed,
|
||||
});
|
||||
|
||||
setSuccess('用户信息更新成功!');
|
||||
} catch (err) {
|
||||
setError('更新用户信息失败: ' + (err instanceof Error ? err.message : String(err)));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
// 调用 API 清除 cookie
|
||||
const response = await fetch('/api/user/me', {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setUserInfo(null);
|
||||
setUpdateData({
|
||||
username: '',
|
||||
avatar_url: '',
|
||||
cover_url: '',
|
||||
country_code: '',
|
||||
pp: 0,
|
||||
global_rank: 0,
|
||||
country_rank: 0,
|
||||
});
|
||||
setSuccess('已退出登录');
|
||||
setError(null);
|
||||
} else {
|
||||
setError('退出登录失败');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('退出登录失败: ' + (err instanceof Error ? err.message : String(err)));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||
<h2 className="text-2xl font-bold mb-4">用户注册与测试</h2>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<div className="mb-4 p-3 bg-green-100 border border-green-400 text-green-700 rounded">
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-semibold mb-2">osu! OAuth 登录</h3>
|
||||
<p className="text-gray-600 mb-4">使用 osu! 账号登录并自动注册用户</p>
|
||||
<button
|
||||
onClick={handleOsuLogin}
|
||||
disabled={loading}
|
||||
className="px-4 py-2 bg-pink-600 text-white rounded hover:bg-pink-700 disabled:opacity-50"
|
||||
>
|
||||
{loading ? '处理中...' : '使用 osu! 账号登录'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-semibold mb-2">搜索用户</h3>
|
||||
<div className="flex flex-col md:flex-row gap-2 mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
checked={searchType === 'osuid'}
|
||||
onChange={() => setSearchType('osuid')}
|
||||
className="mr-2"
|
||||
/>
|
||||
osuid
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
checked={searchType === 'username'}
|
||||
onChange={() => setSearchType('username')}
|
||||
className="mr-2"
|
||||
/>
|
||||
用户名
|
||||
</label>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={searchValue}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
placeholder={searchType === 'osuid' ? '输入 osuid' : '输入用户名'}
|
||||
className="flex-grow px-3 py-2 border rounded"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSearchUser}
|
||||
disabled={loading}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{loading ? '搜索中...' : '搜索用户'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{userInfo && (
|
||||
<div className="border-t pt-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-semibold">用户信息</h3>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700"
|
||||
>
|
||||
退出登录
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<div>
|
||||
<p><strong>ID:</strong> {userInfo.id}</p>
|
||||
<p><strong>osuid:</strong> {userInfo.osuid}</p>
|
||||
<p><strong>用户名:</strong> {userInfo.username}</p>
|
||||
<p><strong>地区:</strong> {userInfo.country_code}</p>
|
||||
<p><strong>PP:</strong> {userInfo.pp.toFixed(2)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p><strong>全球排名:</strong> {userInfo.global_rank}</p>
|
||||
<p><strong>地区排名:</strong> {userInfo.country_rank}</p>
|
||||
<p><strong>赛季:</strong> {userInfo.seasonal}</p>
|
||||
<p><strong>状态:</strong> {userInfo.userState}</p>
|
||||
<p><strong>已批准:</strong> {userInfo.approved ? '是' : '否'}</p>
|
||||
<p><strong>种子:</strong> {userInfo.seed}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{userInfo.avatar_url && (
|
||||
<div className="mb-4">
|
||||
<p className="font-semibold mb-2">头像:</p>
|
||||
<Image src={userInfo.avatar_url}
|
||||
alt="用户头像"
|
||||
className="w-24 h-24 rounded-full"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6">
|
||||
<h4 className="text-md font-semibold mb-3">更新用户信息</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">用户名</label>
|
||||
<input
|
||||
type="text"
|
||||
value={updateData.username}
|
||||
onChange={(e) => setUpdateData({...updateData, username: e.target.value})}
|
||||
className="w-full px-3 py-2 border rounded"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">地区代码</label>
|
||||
<input
|
||||
type="text"
|
||||
value={updateData.country_code}
|
||||
onChange={(e) => setUpdateData({...updateData, country_code: e.target.value})}
|
||||
className="w-full px-3 py-2 border rounded"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">PP</label>
|
||||
<input
|
||||
type="number"
|
||||
value={updateData.pp}
|
||||
onChange={(e) => setUpdateData({...updateData, pp: parseFloat(e.target.value) || 0})}
|
||||
className="w-full px-3 py-2 border rounded"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">全球排名</label>
|
||||
<input
|
||||
type="number"
|
||||
value={updateData.global_rank}
|
||||
onChange={(e) => setUpdateData({...updateData, global_rank: parseInt(e.target.value) || 0})}
|
||||
className="w-full px-3 py-2 border rounded"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">地区排名</label>
|
||||
<input
|
||||
type="number"
|
||||
value={updateData.country_rank}
|
||||
onChange={(e) => setUpdateData({...updateData, country_rank: parseInt(e.target.value) || 0})}
|
||||
className="w-full px-3 py-2 border rounded"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">头像URL</label>
|
||||
<input
|
||||
type="text"
|
||||
value={updateData.avatar_url}
|
||||
onChange={(e) => setUpdateData({...updateData, avatar_url: e.target.value})}
|
||||
className="w-full px-3 py-2 border rounded"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">封面URL</label>
|
||||
<input
|
||||
type="text"
|
||||
value={updateData.cover_url}
|
||||
onChange={(e) => setUpdateData({...updateData, cover_url: e.target.value})}
|
||||
className="w-full px-3 py-2 border rounded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleUpdateProfile}
|
||||
disabled={loading}
|
||||
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50"
|
||||
>
|
||||
{loading ? '更新中...' : '更新用户信息'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import TournamentConfigCard from '@/app/debug/components/TournamentConfigCard';
|
||||
import RegisterUserCard from '@/app/debug/components/RegisterUserCard';
|
||||
|
||||
export default function DebugPage() {
|
||||
return (
|
||||
<div className="container mx-auto p-6">
|
||||
<h1 className="text-3xl font-bold mb-6">调试页面</h1>
|
||||
|
||||
<RegisterUserCard />
|
||||
<TournamentConfigCard />
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { PrismaClient } from '@/app/generated/prisma/client'
|
||||
|
||||
import { PrismaPg } from "@prisma/adapter-pg";
|
||||
|
||||
const connectionString = process.env.DATABASE_URL;
|
||||
const connectionString = process.env.DATABASE_CLIENT_URL;
|
||||
const adapter = new PrismaPg({ connectionString });
|
||||
|
||||
export const prisma = new PrismaClient({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { UserState } from "@/app/generated/prisma/enums";
|
||||
import { UserState,Season } from "@/app/generated/prisma/enums";
|
||||
import { MultiplayerSoloRoom, Team } from "@/app/generated/prisma/client";
|
||||
import { prisma } from "./PrismaClient";
|
||||
|
||||
export interface UserSession {
|
||||
id: number;
|
||||
@@ -14,7 +15,7 @@ export interface UserSession {
|
||||
country_rank: number;
|
||||
approved: number;
|
||||
seed: number;
|
||||
seasonal: number;
|
||||
seasonal: Season;
|
||||
userGroups: number[];
|
||||
SoloRedPlayer?: MultiplayerSoloRoom[];
|
||||
SoloBluePlayer?: MultiplayerSoloRoom[];
|
||||
@@ -23,6 +24,323 @@ export interface UserSession {
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export async function CreateUser(user:UserSession) {
|
||||
|
||||
export async function CreateUser(userData: {
|
||||
osuid: number;
|
||||
username: string;
|
||||
avatar_url?: string;
|
||||
cover_url?: string;
|
||||
country_code: string;
|
||||
pp: number;
|
||||
global_rank: number | null;
|
||||
country_rank: number | null;
|
||||
}): Promise<UserSession> {
|
||||
try {
|
||||
// 获取当前配置
|
||||
const config = await prisma.tournamentConfig.findUnique({
|
||||
where: { id: 1 },
|
||||
});
|
||||
|
||||
const currentSeasonal = config?.current_seasonal || Season.S1;
|
||||
|
||||
const user = await prisma.user.upsert({
|
||||
where: { osuid: userData.osuid },
|
||||
update: {
|
||||
username: userData.username,
|
||||
avatar_url: userData.avatar_url,
|
||||
cover_url: userData.cover_url,
|
||||
country_code: userData.country_code,
|
||||
pp: userData.pp,
|
||||
global_rank: userData.global_rank || 0,
|
||||
country_rank: userData.country_rank || 0,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
create: {
|
||||
osuid: userData.osuid,
|
||||
username: userData.username,
|
||||
avatar_url: userData.avatar_url,
|
||||
cover_url: userData.cover_url,
|
||||
country_code: userData.country_code,
|
||||
pp: userData.pp,
|
||||
global_rank: userData.global_rank || 0,
|
||||
country_rank: userData.country_rank || 0,
|
||||
userState: UserState.ACTIVE,
|
||||
approved: false,
|
||||
seed: 0,
|
||||
seasonal: currentSeasonal,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
userState: user.userState,
|
||||
osuid: user.osuid,
|
||||
username: user.username,
|
||||
avatar_url: user.avatar_url || undefined,
|
||||
cover_url: user.cover_url || undefined,
|
||||
country_code: user.country_code,
|
||||
pp: user.pp,
|
||||
global_rank: user.global_rank,
|
||||
country_rank: user.country_rank,
|
||||
approved: user.approved ? 1 : 0,
|
||||
seed: user.seed,
|
||||
seasonal: user.seasonal,
|
||||
userGroups: [],
|
||||
createdAt: user.createdAt,
|
||||
updatedAt: user.updatedAt,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error creating/updating user:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUserByOsuId(osuid: number): Promise<UserSession | null> {
|
||||
try {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { osuid },
|
||||
include: {
|
||||
userGroups: true,
|
||||
SoloRedPlayer: true,
|
||||
SoloBluePlayer: true,
|
||||
teams: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
userState: user.userState,
|
||||
osuid: user.osuid,
|
||||
username: user.username,
|
||||
avatar_url: user.avatar_url || undefined,
|
||||
cover_url: user.cover_url || undefined,
|
||||
country_code: user.country_code,
|
||||
pp: user.pp,
|
||||
global_rank: user.global_rank,
|
||||
country_rank: user.country_rank,
|
||||
approved: user.approved ? 1 : 0,
|
||||
seed: user.seed,
|
||||
seasonal: user.seasonal,
|
||||
userGroups: user.userGroups.map(group => group.id),
|
||||
SoloRedPlayer: user.SoloRedPlayer,
|
||||
SoloBluePlayer: user.SoloBluePlayer,
|
||||
teams: user.teams,
|
||||
createdAt: user.createdAt,
|
||||
updatedAt: user.updatedAt,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error getting user by osuid:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUserByUsername(username: string): Promise<UserSession | null> {
|
||||
try {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { username },
|
||||
include: {
|
||||
userGroups: true,
|
||||
SoloRedPlayer: true,
|
||||
SoloBluePlayer: true,
|
||||
teams: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
userState: user.userState,
|
||||
osuid: user.osuid,
|
||||
username: user.username,
|
||||
avatar_url: user.avatar_url || undefined,
|
||||
cover_url: user.cover_url || undefined,
|
||||
country_code: user.country_code,
|
||||
pp: user.pp,
|
||||
global_rank: user.global_rank,
|
||||
country_rank: user.country_rank,
|
||||
approved: user.approved ? 1 : 0,
|
||||
seed: user.seed,
|
||||
seasonal: user.seasonal,
|
||||
userGroups: user.userGroups.map(group => group.id),
|
||||
SoloRedPlayer: user.SoloRedPlayer,
|
||||
SoloBluePlayer: user.SoloBluePlayer,
|
||||
teams: user.teams,
|
||||
createdAt: user.createdAt,
|
||||
updatedAt: user.updatedAt,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error getting user by username:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateProfile(osuid: number, updateData: {
|
||||
username?: string;
|
||||
avatar_url?: string;
|
||||
cover_url?: string;
|
||||
country_code?: string;
|
||||
pp?: number;
|
||||
global_rank?: number;
|
||||
country_rank?: number;
|
||||
userState?: UserState;
|
||||
approved?: boolean;
|
||||
seed?: number;
|
||||
seasonal?: Season;
|
||||
}): Promise<UserSession> {
|
||||
try {
|
||||
const user = await prisma.user.update({
|
||||
where: { osuid },
|
||||
data: {
|
||||
...updateData,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
include: {
|
||||
userGroups: true,
|
||||
SoloRedPlayer: true,
|
||||
SoloBluePlayer: true,
|
||||
teams: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
userState: user.userState,
|
||||
osuid: user.osuid,
|
||||
username: user.username,
|
||||
avatar_url: user.avatar_url || undefined,
|
||||
cover_url: user.cover_url || undefined,
|
||||
country_code: user.country_code,
|
||||
pp: user.pp,
|
||||
global_rank: user.global_rank,
|
||||
country_rank: user.country_rank,
|
||||
approved: user.approved ? 1 : 0,
|
||||
seed: user.seed,
|
||||
seasonal: user.seasonal,
|
||||
userGroups: user.userGroups.map(group => group.id),
|
||||
SoloRedPlayer: user.SoloRedPlayer,
|
||||
SoloBluePlayer: user.SoloBluePlayer,
|
||||
teams: user.teams,
|
||||
createdAt: user.createdAt,
|
||||
updatedAt: user.updatedAt,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error updating user profile:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function addUserToGroup(userId: number, groupId: number): Promise<UserSession> {
|
||||
try {
|
||||
const user = await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
userGroups: {
|
||||
connect: { id: groupId },
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
include: {
|
||||
userGroups: true,
|
||||
SoloRedPlayer: true,
|
||||
SoloBluePlayer: true,
|
||||
teams: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
userState: user.userState,
|
||||
osuid: user.osuid,
|
||||
username: user.username,
|
||||
avatar_url: user.avatar_url || undefined,
|
||||
cover_url: user.cover_url || undefined,
|
||||
country_code: user.country_code,
|
||||
pp: user.pp,
|
||||
global_rank: user.global_rank,
|
||||
country_rank: user.country_rank,
|
||||
approved: user.approved ? 1 : 0,
|
||||
seed: user.seed,
|
||||
seasonal: user.seasonal,
|
||||
userGroups: user.userGroups.map(group => group.id),
|
||||
SoloRedPlayer: user.SoloRedPlayer,
|
||||
SoloBluePlayer: user.SoloBluePlayer,
|
||||
teams: user.teams,
|
||||
createdAt: user.createdAt,
|
||||
updatedAt: user.updatedAt,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error adding user to group:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeUserFromGroup(userId: number, groupId: number): Promise<UserSession> {
|
||||
try {
|
||||
const user = await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
userGroups: {
|
||||
disconnect: { id: groupId },
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
include: {
|
||||
userGroups: true,
|
||||
SoloRedPlayer: true,
|
||||
SoloBluePlayer: true,
|
||||
teams: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
userState: user.userState,
|
||||
osuid: user.osuid,
|
||||
username: user.username,
|
||||
avatar_url: user.avatar_url || undefined,
|
||||
cover_url: user.cover_url || undefined,
|
||||
country_code: user.country_code,
|
||||
pp: user.pp,
|
||||
global_rank: user.global_rank,
|
||||
country_rank: user.country_rank,
|
||||
approved: user.approved ? 1 : 0,
|
||||
seed: user.seed,
|
||||
seasonal: user.seasonal,
|
||||
userGroups: user.userGroups.map(group => group.id),
|
||||
SoloRedPlayer: user.SoloRedPlayer,
|
||||
SoloBluePlayer: user.SoloBluePlayer,
|
||||
teams: user.teams,
|
||||
createdAt: user.createdAt,
|
||||
updatedAt: user.updatedAt,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error removing user from group:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUserGroups(userId: number): Promise<number[]> {
|
||||
try {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: {
|
||||
userGroups: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return user.userGroups.map(group => group.id);
|
||||
} catch (error) {
|
||||
console.error('Error getting user groups:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// 这些环境变量只在服务器端可用
|
||||
const OSU_CLIENT_ID = process.env.OSU_CLIENT_ID || '';
|
||||
const OSU_CLIENT_SECRET = process.env.OSU_CLIENT_SECRET || '';
|
||||
const OSU_REDIRECT_URI = process.env.OSU_REDIRECT_URI || 'http://localhost:3000/auth/osu/callback';
|
||||
const OSU_REDIRECT_URI = process.env.OSU_REDIRECT_URI || 'http://localhost:3000/api/auth/callback/osu';
|
||||
|
||||
export function getOsuAuthUrl() {
|
||||
if (!OSU_CLIENT_ID || !OSU_CLIENT_SECRET) {
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'a.ppy.sh',
|
||||
pathname: '/**',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'assets.ppy.sh',
|
||||
pathname: '/**',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'osu.ppy.sh',
|
||||
pathname: '/**',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
307
prisma/migrations/20260205015503_init/migration.sql
Normal file
307
prisma/migrations/20260205015503_init/migration.sql
Normal file
@@ -0,0 +1,307 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "BeatmapSelectMod" AS ENUM ('NM', 'HD', 'HR', 'DT', 'LZ', 'TB');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "MultiplayerRoomType" AS ENUM ('SOLO', 'TEAM_VS');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TeamColor" AS ENUM ('blue_team', 'red_team');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TeamState" AS ENUM ('ACTIVE', 'UNAPPROVED', 'APPROVED', 'BANNED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "UserState" AS ENUM ('ACTIVE', 'REGISTERED', 'ABANDONED', 'BANNED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "UserGroupType" AS ENUM ('HOST', 'ADMIN', 'POOLER', 'STREAMER', 'TESTER', 'GFX', 'SHEETER', 'COMMENTATOR', 'MAPPER', 'REFEREE', 'PLAYER');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "Season" AS ENUM ('S1', 'S2');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "Category" AS ENUM ('QUA', 'RO16', 'QF', 'SF', 'F', 'GF');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Beatmap" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"beatmap_id" INTEGER NOT NULL,
|
||||
"beatmapset_id" INTEGER NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"title_unicode" TEXT NOT NULL,
|
||||
"artist" TEXT NOT NULL,
|
||||
"artist_unicode" TEXT NOT NULL,
|
||||
"creator" TEXT NOT NULL,
|
||||
"cover_url" TEXT NOT NULL,
|
||||
"version" TEXT NOT NULL,
|
||||
"ar" DOUBLE PRECISION NOT NULL,
|
||||
"od" DOUBLE PRECISION NOT NULL,
|
||||
"cs" DOUBLE PRECISION NOT NULL,
|
||||
"hp" DOUBLE PRECISION NOT NULL,
|
||||
"bpm" DOUBLE PRECISION NOT NULL,
|
||||
"length" INTEGER NOT NULL,
|
||||
"max_combo" INTEGER NOT NULL,
|
||||
"star_rating" DOUBLE PRECISION NOT NULL,
|
||||
"mod" JSONB NOT NULL DEFAULT '[]',
|
||||
"selectMod" "BeatmapSelectMod" NOT NULL DEFAULT 'NM',
|
||||
"selectModSlot" INTEGER NOT NULL DEFAULT 1,
|
||||
"selectByosuId" INTEGER NOT NULL,
|
||||
"selectNote" TEXT NOT NULL DEFAULT '',
|
||||
"approved" BOOLEAN NOT NULL DEFAULT false,
|
||||
"needTest" BOOLEAN NOT NULL DEFAULT false,
|
||||
"isCustom" BOOLEAN NOT NULL DEFAULT false,
|
||||
"isOriginal" BOOLEAN NOT NULL DEFAULT false,
|
||||
"season" INTEGER NOT NULL DEFAULT 1,
|
||||
"category" "Category" NOT NULL DEFAULT 'QUA',
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Beatmap_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "BeatmapComment" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"beatmap_id" INTEGER NOT NULL,
|
||||
"comment" TEXT NOT NULL DEFAULT '',
|
||||
"osuid" INTEGER NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "BeatmapComment_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Message" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"sender_id" INTEGER NOT NULL,
|
||||
"receiver_id" INTEGER NOT NULL,
|
||||
"room_id" INTEGER,
|
||||
"isRead" BOOLEAN NOT NULL DEFAULT false,
|
||||
"isDeleted" BOOLEAN NOT NULL DEFAULT false,
|
||||
"isSystemMessage" BOOLEAN NOT NULL DEFAULT false,
|
||||
"isAccepted" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Message_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "MultiplayerRoom" (
|
||||
"room_id" SERIAL NOT NULL,
|
||||
"type" "MultiplayerRoomType" NOT NULL DEFAULT 'SOLO',
|
||||
"season" "Season" NOT NULL DEFAULT 'S1',
|
||||
"category" "Category" NOT NULL DEFAULT 'QUA',
|
||||
"multiplayerSoloRoomRoom_id" INTEGER,
|
||||
"multiplayerTeamvsRoomRoom_id" INTEGER,
|
||||
"score_red" INTEGER NOT NULL DEFAULT 0,
|
||||
"score_blue" INTEGER NOT NULL DEFAULT 0,
|
||||
"start_time" TIMESTAMP(3) NOT NULL,
|
||||
"end_time" TIMESTAMP(3) NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "MultiplayerRoom_pkey" PRIMARY KEY ("room_id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "MultiplayerSoloRoom" (
|
||||
"room_id" SERIAL NOT NULL,
|
||||
"player_red_id" INTEGER NOT NULL,
|
||||
"player_blue_id" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "MultiplayerSoloRoom_pkey" PRIMARY KEY ("room_id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "MultiplayerTeamvsRoom" (
|
||||
"room_id" SERIAL NOT NULL,
|
||||
"team_red_id" INTEGER NOT NULL,
|
||||
"team_blue_id" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "MultiplayerTeamvsRoom_pkey" PRIMARY KEY ("room_id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Score" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"user_id" INTEGER NOT NULL,
|
||||
"username" TEXT NOT NULL,
|
||||
"beatmap_id" INTEGER NOT NULL,
|
||||
"total_score" INTEGER NOT NULL DEFAULT 0,
|
||||
"accuracy" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"max_combo" INTEGER NOT NULL DEFAULT 0,
|
||||
"mods" JSONB NOT NULL DEFAULT '[]',
|
||||
"rank" INTEGER NOT NULL DEFAULT 0,
|
||||
"pp" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"passed" BOOLEAN NOT NULL DEFAULT true,
|
||||
"statistics" JSONB NOT NULL DEFAULT '[]',
|
||||
"ended_at" TIMESTAMP(3) NOT NULL,
|
||||
"saved_at" TIMESTAMP(3) NOT NULL,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Score_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Team" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"banner_url" TEXT NOT NULL,
|
||||
"teamColor" "TeamColor" NOT NULL,
|
||||
"teamState" "TeamState" NOT NULL DEFAULT 'ACTIVE',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Team_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"userState" "UserState" NOT NULL DEFAULT 'ACTIVE',
|
||||
"osuid" INTEGER NOT NULL,
|
||||
"username" TEXT NOT NULL,
|
||||
"avatar_url" TEXT,
|
||||
"cover_url" TEXT,
|
||||
"country_code" TEXT NOT NULL,
|
||||
"pp" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"global_rank" INTEGER NOT NULL DEFAULT 0,
|
||||
"country_rank" INTEGER NOT NULL DEFAULT 0,
|
||||
"approved" BOOLEAN NOT NULL DEFAULT false,
|
||||
"seed" INTEGER NOT NULL DEFAULT 0,
|
||||
"seasonal" "Season" NOT NULL DEFAULT 'S1',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "UserGroup" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"group" "UserGroupType" NOT NULL DEFAULT 'PLAYER',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "UserGroup_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TournamentConfig" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"tournament_name" TEXT NOT NULL DEFAULT 'AstarCup',
|
||||
"max_pp_for_registration" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"min_pp_for_registration" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"current_seasonal" "Season" NOT NULL DEFAULT 'S1',
|
||||
"current_category" "Category" NOT NULL DEFAULT 'QUA',
|
||||
"canRegister" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "TournamentConfig_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_TeamToUser" (
|
||||
"A" INTEGER NOT NULL,
|
||||
"B" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "_TeamToUser_AB_pkey" PRIMARY KEY ("A","B")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_UserToUserGroup" (
|
||||
"A" INTEGER NOT NULL,
|
||||
"B" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "_UserToUserGroup_AB_pkey" PRIMARY KEY ("A","B")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Beatmap_beatmap_id_key" ON "Beatmap"("beatmap_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Beatmap_beatmapset_id_key" ON "Beatmap"("beatmapset_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "BeatmapComment_beatmap_id_key" ON "BeatmapComment"("beatmap_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "BeatmapComment_osuid_key" ON "BeatmapComment"("osuid");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "MultiplayerSoloRoom_player_red_id_key" ON "MultiplayerSoloRoom"("player_red_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "MultiplayerSoloRoom_player_blue_id_key" ON "MultiplayerSoloRoom"("player_blue_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "MultiplayerTeamvsRoom_team_red_id_key" ON "MultiplayerTeamvsRoom"("team_red_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "MultiplayerTeamvsRoom_team_blue_id_key" ON "MultiplayerTeamvsRoom"("team_blue_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_osuid_key" ON "User"("osuid");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_TeamToUser_B_index" ON "_TeamToUser"("B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_UserToUserGroup_B_index" ON "_UserToUserGroup"("B");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Beatmap" ADD CONSTRAINT "Beatmap_selectByosuId_fkey" FOREIGN KEY ("selectByosuId") REFERENCES "User"("osuid") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "BeatmapComment" ADD CONSTRAINT "BeatmapComment_beatmap_id_fkey" FOREIGN KEY ("beatmap_id") REFERENCES "Beatmap"("beatmap_id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "BeatmapComment" ADD CONSTRAINT "BeatmapComment_osuid_fkey" FOREIGN KEY ("osuid") REFERENCES "User"("osuid") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Message" ADD CONSTRAINT "Message_sender_id_fkey" FOREIGN KEY ("sender_id") REFERENCES "User"("osuid") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Message" ADD CONSTRAINT "Message_receiver_id_fkey" FOREIGN KEY ("receiver_id") REFERENCES "User"("osuid") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Message" ADD CONSTRAINT "Message_room_id_fkey" FOREIGN KEY ("room_id") REFERENCES "MultiplayerRoom"("room_id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "MultiplayerRoom" ADD CONSTRAINT "MultiplayerRoom_multiplayerSoloRoomRoom_id_fkey" FOREIGN KEY ("multiplayerSoloRoomRoom_id") REFERENCES "MultiplayerSoloRoom"("room_id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "MultiplayerRoom" ADD CONSTRAINT "MultiplayerRoom_multiplayerTeamvsRoomRoom_id_fkey" FOREIGN KEY ("multiplayerTeamvsRoomRoom_id") REFERENCES "MultiplayerTeamvsRoom"("room_id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "MultiplayerSoloRoom" ADD CONSTRAINT "MultiplayerSoloRoom_player_red_id_fkey" FOREIGN KEY ("player_red_id") REFERENCES "User"("osuid") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "MultiplayerSoloRoom" ADD CONSTRAINT "MultiplayerSoloRoom_player_blue_id_fkey" FOREIGN KEY ("player_blue_id") REFERENCES "User"("osuid") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "MultiplayerTeamvsRoom" ADD CONSTRAINT "MultiplayerTeamvsRoom_team_red_id_fkey" FOREIGN KEY ("team_red_id") REFERENCES "Team"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "MultiplayerTeamvsRoom" ADD CONSTRAINT "MultiplayerTeamvsRoom_team_blue_id_fkey" FOREIGN KEY ("team_blue_id") REFERENCES "Team"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_TeamToUser" ADD CONSTRAINT "_TeamToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_TeamToUser" ADD CONSTRAINT "_TeamToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_UserToUserGroup" ADD CONSTRAINT "_UserToUserGroup_A_fkey" FOREIGN KEY ("A") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_UserToUserGroup" ADD CONSTRAINT "_UserToUserGroup_B_fkey" FOREIGN KEY ("B") REFERENCES "UserGroup"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
@@ -11,7 +11,7 @@ model User {
|
||||
country_rank Int @default(0)
|
||||
approved Boolean @default(false)
|
||||
seed Int @default(0)
|
||||
seasonal Int @default(1)
|
||||
seasonal Season @default(S1)
|
||||
SoloRedPlayer MultiplayerSoloRoom[] @relation("SoloRedPlayer")
|
||||
SoloBluePlayer MultiplayerSoloRoom[] @relation("SoloBluePlayer")
|
||||
teams Team[]
|
||||
|
||||
Reference in New Issue
Block a user