From 164bb5c341038e039999e203c7fd117e52dfd480 Mon Sep 17 00:00:00 2001 From: AeCw Date: Thu, 5 Feb 2026 12:05:12 +0800 Subject: [PATCH] update --- README.md | 9 + app/api/auth/callback/osu/route.ts | 88 ++++ app/api/auth/getAuthUrl/route.ts | 15 + app/api/user/me/route.ts | 76 +++ app/api/user/search/route.ts | 51 ++ app/api/user/update/route.ts | 25 + app/debug/api/tournament-config/route.ts | 19 - app/debug/components/RegisterUserCard.tsx | 473 ++++++++++++++++++ app/debug/page.tsx | 4 +- app/lib/PrismaClient.ts | 2 +- app/lib/UserOperation.ts | 326 +++++++++++- app/lib/osuAuth.ts | 3 +- next.config.ts | 20 +- .../20260205015503_init/migration.sql | 307 ++++++++++++ prisma/migrations/migration_lock.toml | 3 + prisma/models/User.prisma | 2 +- 16 files changed, 1395 insertions(+), 28 deletions(-) create mode 100644 app/api/auth/callback/osu/route.ts create mode 100644 app/api/auth/getAuthUrl/route.ts create mode 100644 app/api/user/me/route.ts create mode 100644 app/api/user/search/route.ts create mode 100644 app/api/user/update/route.ts delete mode 100644 app/debug/api/tournament-config/route.ts create mode 100644 app/debug/components/RegisterUserCard.tsx create mode 100644 prisma/migrations/20260205015503_init/migration.sql create mode 100644 prisma/migrations/migration_lock.toml diff --git a/README.md b/README.md index 9af0fd7..8a35d3e 100644 --- a/README.md +++ b/README.md @@ -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="" +`` \ No newline at end of file diff --git a/app/api/auth/callback/osu/route.ts b/app/api/auth/callback/osu/route.ts new file mode 100644 index 0000000..aead15c --- /dev/null +++ b/app/api/auth/callback/osu/route.ts @@ -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) + ); + } +} \ No newline at end of file diff --git a/app/api/auth/getAuthUrl/route.ts b/app/api/auth/getAuthUrl/route.ts new file mode 100644 index 0000000..c14ec22 --- /dev/null +++ b/app/api/auth/getAuthUrl/route.ts @@ -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')) + ); + } +} \ No newline at end of file diff --git a/app/api/user/me/route.ts b/app/api/user/me/route.ts new file mode 100644 index 0000000..4c89c58 --- /dev/null +++ b/app/api/user/me/route.ts @@ -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 } + ); + } +} \ No newline at end of file diff --git a/app/api/user/search/route.ts b/app/api/user/search/route.ts new file mode 100644 index 0000000..45bdc82 --- /dev/null +++ b/app/api/user/search/route.ts @@ -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 } + ); + } +} \ No newline at end of file diff --git a/app/api/user/update/route.ts b/app/api/user/update/route.ts new file mode 100644 index 0000000..a2193c4 --- /dev/null +++ b/app/api/user/update/route.ts @@ -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 } + ); + } +} \ No newline at end of file diff --git a/app/debug/api/tournament-config/route.ts b/app/debug/api/tournament-config/route.ts deleted file mode 100644 index 898d3e0..0000000 --- a/app/debug/api/tournament-config/route.ts +++ /dev/null @@ -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 } - ); - } -} diff --git a/app/debug/components/RegisterUserCard.tsx b/app/debug/components/RegisterUserCard.tsx new file mode 100644 index 0000000..31b9ff0 --- /dev/null +++ b/app/debug/components/RegisterUserCard.tsx @@ -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(null); + const [success, setSuccess] = useState(null); + const [userInfo, setUserInfo] = useState(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 ( +
+

用户注册与测试

+ + {error && ( +
+ {error} +
+ )} + + {success && ( +
+ {success} +
+ )} + +
+

osu! OAuth 登录

+

使用 osu! 账号登录并自动注册用户

+ +
+ +
+

搜索用户

+
+
+ + +
+ setSearchValue(e.target.value)} + placeholder={searchType === 'osuid' ? '输入 osuid' : '输入用户名'} + className="flex-grow px-3 py-2 border rounded" + /> + +
+
+ + {userInfo && ( +
+
+

用户信息

+ +
+ +
+
+

ID: {userInfo.id}

+

osuid: {userInfo.osuid}

+

用户名: {userInfo.username}

+

地区: {userInfo.country_code}

+

PP: {userInfo.pp.toFixed(2)}

+
+
+

全球排名: {userInfo.global_rank}

+

地区排名: {userInfo.country_rank}

+

赛季: {userInfo.seasonal}

+

状态: {userInfo.userState}

+

已批准: {userInfo.approved ? '是' : '否'}

+

种子: {userInfo.seed}

+
+
+ + {userInfo.avatar_url && ( +
+

头像:

+ 用户头像 + +
+ )} + +
+

更新用户信息

+
+
+ + setUpdateData({...updateData, username: e.target.value})} + className="w-full px-3 py-2 border rounded" + /> +
+
+ + setUpdateData({...updateData, country_code: e.target.value})} + className="w-full px-3 py-2 border rounded" + /> +
+
+ + setUpdateData({...updateData, pp: parseFloat(e.target.value) || 0})} + className="w-full px-3 py-2 border rounded" + /> +
+
+ + setUpdateData({...updateData, global_rank: parseInt(e.target.value) || 0})} + className="w-full px-3 py-2 border rounded" + /> +
+
+ + setUpdateData({...updateData, country_rank: parseInt(e.target.value) || 0})} + className="w-full px-3 py-2 border rounded" + /> +
+
+ + setUpdateData({...updateData, avatar_url: e.target.value})} + className="w-full px-3 py-2 border rounded" + /> +
+
+ + setUpdateData({...updateData, cover_url: e.target.value})} + className="w-full px-3 py-2 border rounded" + /> +
+
+ + +
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/app/debug/page.tsx b/app/debug/page.tsx index 1b7c022..68cf876 100644 --- a/app/debug/page.tsx +++ b/app/debug/page.tsx @@ -1,12 +1,14 @@ import TournamentConfigCard from '@/app/debug/components/TournamentConfigCard'; +import RegisterUserCard from '@/app/debug/components/RegisterUserCard'; export default function DebugPage() { return (

调试页面

+
); -} \ No newline at end of file +} diff --git a/app/lib/PrismaClient.ts b/app/lib/PrismaClient.ts index 041b4ec..3a5d6f0 100644 --- a/app/lib/PrismaClient.ts +++ b/app/lib/PrismaClient.ts @@ -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({ diff --git a/app/lib/UserOperation.ts b/app/lib/UserOperation.ts index 5e301e2..ae3001a 100644 --- a/app/lib/UserOperation.ts +++ b/app/lib/UserOperation.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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; + } } \ No newline at end of file diff --git a/app/lib/osuAuth.ts b/app/lib/osuAuth.ts index 2679c4c..9bb504a 100644 --- a/app/lib/osuAuth.ts +++ b/app/lib/osuAuth.ts @@ -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) { diff --git a/next.config.ts b/next.config.ts index e9ffa30..68636ac 100644 --- a/next.config.ts +++ b/next.config.ts @@ -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; diff --git a/prisma/migrations/20260205015503_init/migration.sql b/prisma/migrations/20260205015503_init/migration.sql new file mode 100644 index 0000000..571d56f --- /dev/null +++ b/prisma/migrations/20260205015503_init/migration.sql @@ -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; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -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" diff --git a/prisma/models/User.prisma b/prisma/models/User.prisma index 7156aea..4893ad5 100644 --- a/prisma/models/User.prisma +++ b/prisma/models/User.prisma @@ -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[]