fix: resolve multiple runtime and build issues
- Fix network binding to allow external access (0.0.0.0) - Upgrade to Node.js 20.x for Next.js 16 compatibility - Fix next-auth v4 configuration and session handling - Add Steam client secret for Steam OAuth provider - Fix Prisma schema unique constraint syntax - Fix database creation script for automated deployment - Fix game search API to use new IStoreService endpoint - Fix session auth in API routes for Steam linking - Add TypeScript types for next-auth session
This commit is contained in:
19
deploy.sh
Normal file → Executable file
19
deploy.sh
Normal file → Executable file
@@ -3,6 +3,8 @@ set -e
|
|||||||
|
|
||||||
DOMAIN="${1:-192.168.2.175:3000}"
|
DOMAIN="${1:-192.168.2.175:3000}"
|
||||||
|
|
||||||
|
export PATH="$HOME/.local/node/bin:$PATH"
|
||||||
|
|
||||||
echo "=== FairReview Deployment Script ==="
|
echo "=== FairReview Deployment Script ==="
|
||||||
echo "Target domain: $DOMAIN"
|
echo "Target domain: $DOMAIN"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -22,6 +24,9 @@ fi
|
|||||||
|
|
||||||
source .postgres-env
|
source .postgres-env
|
||||||
|
|
||||||
|
# Export database credentials for Node.js scripts
|
||||||
|
export DB_HOST DB_PORT DB_USER DB_PASS DB_NAME
|
||||||
|
|
||||||
# Validate credentials
|
# Validate credentials
|
||||||
if [ -z "$DB_PASS" ] || [ "$DB_PASS" = "your_password_here" ]; then
|
if [ -z "$DB_PASS" ] || [ "$DB_PASS" = "your_password_here" ]; then
|
||||||
echo "Error: Please update .postgres-env with your actual database password."
|
echo "Error: Please update .postgres-env with your actual database password."
|
||||||
@@ -47,7 +52,7 @@ fi
|
|||||||
echo ""
|
echo ""
|
||||||
echo "=== Installing required packages ==="
|
echo "=== Installing required packages ==="
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install -y curl build-essential git
|
sudo apt install -y curl build-essential git postgresql-client
|
||||||
|
|
||||||
# Install PM2 globally
|
# Install PM2 globally
|
||||||
echo ""
|
echo ""
|
||||||
@@ -82,10 +87,18 @@ echo " Database: ${DB_HOST}:${DB_PORT}/${DB_NAME}"
|
|||||||
echo ""
|
echo ""
|
||||||
echo "IMPORTANT: Update STEAM_API_KEY in .env.local with your Steam Web API key!"
|
echo "IMPORTANT: Update STEAM_API_KEY in .env.local with your Steam Web API key!"
|
||||||
|
|
||||||
# Generate Prisma client and push schema
|
# Generate Prisma client
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Setting up database ==="
|
echo "=== Setting up database ==="
|
||||||
npx prisma generate
|
npx prisma generate
|
||||||
|
|
||||||
|
# Create database if it doesn't exist
|
||||||
|
echo ""
|
||||||
|
echo "=== Creating database if not exists ==="
|
||||||
|
node scripts/create-db.js
|
||||||
|
|
||||||
|
# Push schema to database
|
||||||
|
export DATABASE_URL="postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=require"
|
||||||
npx prisma db push
|
npx prisma db push
|
||||||
|
|
||||||
# Build for production
|
# Build for production
|
||||||
@@ -98,7 +111,7 @@ echo ""
|
|||||||
echo "=== Starting application with PM2 ==="
|
echo "=== Starting application with PM2 ==="
|
||||||
pm2 stop fairreview 2>/dev/null || true
|
pm2 stop fairreview 2>/dev/null || true
|
||||||
pm2 delete fairreview 2>/dev/null || true
|
pm2 delete fairreview 2>/dev/null || true
|
||||||
pm2 start npm --name "fairreview" -- run start
|
pm2 start npm --name "fairreview" -- start -- -H 0.0.0.0
|
||||||
|
|
||||||
# Save PM2 config for restart on reboot
|
# Save PM2 config for restart on reboot
|
||||||
pm2 save
|
pm2 save
|
||||||
|
|||||||
932
package-lock.json
generated
932
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,17 +9,24 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@prisma/client": "^6.19.2",
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
|
"next-auth": "^4.24.13",
|
||||||
|
"next-auth-steam": "^0.4.0",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3"
|
"react-dom": "19.2.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.1.6",
|
"eslint-config-next": "16.1.6",
|
||||||
|
"pg": "^8.18.0",
|
||||||
|
"prisma": "^6.19.2",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,6 @@ model Vote {
|
|||||||
user User @relation(fields: [userId], references: [id])
|
user User @relation(fields: [userId], references: [id])
|
||||||
review Review @relation(fields: [reviewId], references: [id])
|
review Review @relation(fields: [reviewId], references: [id])
|
||||||
|
|
||||||
@@unique([userId, reviewId]) @map("user_review_unique")
|
@@unique([userId, reviewId], name: "user_review_unique")
|
||||||
@@map("votes")
|
@@map("votes")
|
||||||
}
|
}
|
||||||
|
|||||||
48
scripts/create-db.js
Normal file
48
scripts/create-db.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
const { Client } = require('pg');
|
||||||
|
|
||||||
|
async function createDatabase() {
|
||||||
|
const dbHost = process.env.DB_HOST || '192.168.2.175';
|
||||||
|
const dbPort = process.env.DB_PORT || 5432;
|
||||||
|
const dbUser = process.env.DB_USER || 'zhuma';
|
||||||
|
const dbPass = process.env.DB_PASS;
|
||||||
|
const dbName = process.env.DB_NAME || 'fairreview';
|
||||||
|
|
||||||
|
if (!dbPass) {
|
||||||
|
console.error('Error: DB_PASS environment variable not set');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new Client({
|
||||||
|
host: dbHost,
|
||||||
|
port: parseInt(dbPort),
|
||||||
|
user: dbUser,
|
||||||
|
password: dbPass,
|
||||||
|
database: 'postgres',
|
||||||
|
ssl: {
|
||||||
|
rejectUnauthorized: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
const result = await client.query(
|
||||||
|
"SELECT 1 FROM pg_database WHERE datname = $1",
|
||||||
|
[dbName]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
await client.query(`CREATE DATABASE ${dbName}`);
|
||||||
|
console.log(`Database ${dbName} created successfully!`);
|
||||||
|
} else {
|
||||||
|
console.log(`Database ${dbName} already exists.`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createDatabase();
|
||||||
@@ -1,3 +1,19 @@
|
|||||||
import { handlers } from '@/lib/auth'
|
import NextAuth from 'next-auth'
|
||||||
|
import { authOptions } from '@/lib/auth'
|
||||||
|
import SteamProvider from 'next-auth-steam'
|
||||||
|
import { NextRequest } from 'next/server'
|
||||||
|
|
||||||
export const { GET, POST } = handlers
|
async function auth(req: NextRequest, ctx: { params: Promise<{ nextauth: string[] }> }) {
|
||||||
|
const resolvedParams = await ctx.params
|
||||||
|
return NextAuth(req, { params: resolvedParams }, {
|
||||||
|
...authOptions,
|
||||||
|
providers: [
|
||||||
|
...authOptions.providers,
|
||||||
|
SteamProvider(req, {
|
||||||
|
clientSecret: process.env.STEAM_CLIENT_SECRET || ''
|
||||||
|
})
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export { auth as GET, auth as POST }
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export async function GET(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let game = await prisma.game.findUnique({
|
let game = await prisma.game.findUnique({
|
||||||
where: { app_id: appId }
|
where: { appId: appId }
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!game) {
|
if (!game) {
|
||||||
@@ -26,13 +26,13 @@ export async function GET(
|
|||||||
|
|
||||||
game = await prisma.game.create({
|
game = await prisma.game.create({
|
||||||
data: {
|
data: {
|
||||||
app_id: appId,
|
appId: appId,
|
||||||
name: steamData.name,
|
name: steamData.name,
|
||||||
description: steamData.short_description,
|
description: steamData.short_description,
|
||||||
header_image: steamData.header_image,
|
headerImage: steamData.header_image,
|
||||||
capsule_image: steamData.capsule_image,
|
capsuleImage: steamData.capsule_image,
|
||||||
background_image: steamData.background,
|
backgroundImage: steamData.background,
|
||||||
release_date: steamData.release_date?.date,
|
releaseDate: steamData.release_date?.date,
|
||||||
developers: steamData.developers || [],
|
developers: steamData.developers || [],
|
||||||
publishers: steamData.publishers || [],
|
publishers: steamData.publishers || [],
|
||||||
genres: steamData.genres?.map(g => g.description) || []
|
genres: steamData.genres?.map(g => g.description) || []
|
||||||
@@ -41,12 +41,12 @@ export async function GET(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
app_id: game.app_id,
|
app_id: game.appId,
|
||||||
name: game.name,
|
name: game.name,
|
||||||
description: game.description,
|
description: game.description,
|
||||||
header_image: game.header_image,
|
header_image: game.headerImage,
|
||||||
background_image: game.background_image,
|
background_image: game.backgroundImage,
|
||||||
release_date: game.release_date,
|
release_date: game.releaseDate,
|
||||||
developers: game.developers,
|
developers: game.developers,
|
||||||
publishers: game.publishers,
|
publishers: game.publishers,
|
||||||
genres: game.genres
|
genres: game.genres
|
||||||
|
|||||||
@@ -18,15 +18,15 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
app_id: true,
|
appId: true,
|
||||||
name: true,
|
name: true,
|
||||||
header_image: true
|
headerImage: true
|
||||||
},
|
},
|
||||||
take: 10
|
take: 10
|
||||||
})
|
})
|
||||||
|
|
||||||
if (cachedGames.length > 0) {
|
if (cachedGames.length > 0) {
|
||||||
return NextResponse.json(cachedGames)
|
return NextResponse.json(cachedGames.map((g: any) => ({ app_id: g.appId, name: g.name, header_image: g.headerImage })))
|
||||||
}
|
}
|
||||||
|
|
||||||
const steamGames = await searchSteamGames(query)
|
const steamGames = await searchSteamGames(query)
|
||||||
|
|||||||
@@ -30,29 +30,29 @@ export async function POST(
|
|||||||
const existingVote = await prisma.vote.findUnique({
|
const existingVote = await prisma.vote.findUnique({
|
||||||
where: {
|
where: {
|
||||||
user_review_unique: {
|
user_review_unique: {
|
||||||
user_id: session.user.id,
|
userId: session.user.id,
|
||||||
review_id: resolvedParams.id
|
reviewId: resolvedParams.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (existingVote) {
|
if (existingVote) {
|
||||||
if (existingVote.vote_type === voteType) {
|
if (existingVote.voteType === voteType) {
|
||||||
await prisma.vote.delete({
|
await prisma.vote.delete({
|
||||||
where: { id: existingVote.id }
|
where: { id: existingVote.id }
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
await prisma.vote.update({
|
await prisma.vote.update({
|
||||||
where: { id: existingVote.id },
|
where: { id: existingVote.id },
|
||||||
data: { vote_type: voteType }
|
data: { voteType: voteType }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await prisma.vote.create({
|
await prisma.vote.create({
|
||||||
data: {
|
data: {
|
||||||
user_id: session.user.id,
|
userId: session.user.id,
|
||||||
review_id: resolvedParams.id,
|
reviewId: resolvedParams.id,
|
||||||
vote_type: voteType
|
voteType: voteType
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export async function GET(request: NextRequest) {
|
|||||||
const where: Record<string, unknown> = {}
|
const where: Record<string, unknown> = {}
|
||||||
|
|
||||||
if (appId) {
|
if (appId) {
|
||||||
where.app_id = parseInt(appId)
|
where.appId = parseInt(appId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const reviews = await prisma.review.findMany({
|
const reviews = await prisma.review.findMany({
|
||||||
@@ -34,16 +34,16 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
created_at: 'desc'
|
createdAt: 'desc'
|
||||||
},
|
},
|
||||||
take: limit
|
take: limit
|
||||||
})
|
})
|
||||||
|
|
||||||
const formattedReviews = reviews.map(review => {
|
const formattedReviews = reviews.map((review: any) => {
|
||||||
const upvotes = review.votes.filter(v => v.vote_type === 1).length
|
const upvotes = review.votes.filter((v: any) => v.voteType === 1).length
|
||||||
const downvotes = review.votes.filter(v => v.vote_type === -1).length
|
const downvotes = review.votes.filter((v: any) => v.voteType === -1).length
|
||||||
const userVote = userId
|
const userVote = userId
|
||||||
? review.votes.find(v => v.user_id === userId)?.vote_type || null
|
? review.votes.find((v: any) => v.userId === userId)?.voteType || null
|
||||||
: null
|
: null
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -52,11 +52,11 @@ export async function GET(request: NextRequest) {
|
|||||||
avatar_url: review.user.steamAvatar,
|
avatar_url: review.user.steamAvatar,
|
||||||
content: review.content,
|
content: review.content,
|
||||||
rating: review.rating,
|
rating: review.rating,
|
||||||
playtime_hours: review.playtime_hours,
|
playtime_hours: review.playtimeHours,
|
||||||
upvotes,
|
upvotes,
|
||||||
downvotes,
|
downvotes,
|
||||||
user_vote: userVote,
|
user_vote: userVote,
|
||||||
created_at: review.created_at.toISOString(),
|
created_at: review.createdAt.toISOString(),
|
||||||
game_name: review.game?.name
|
game_name: review.game?.name
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -89,8 +89,8 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
const existingReview = await prisma.review.findFirst({
|
const existingReview = await prisma.review.findFirst({
|
||||||
where: {
|
where: {
|
||||||
user_id: session.user.id,
|
userId: session.user.id,
|
||||||
app_id: appId
|
appId: appId
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -102,16 +102,16 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const game = await prisma.game.findUnique({
|
const game = await prisma.game.findUnique({
|
||||||
where: { app_id: appId }
|
where: { appId: appId }
|
||||||
})
|
})
|
||||||
|
|
||||||
const review = await prisma.review.create({
|
const review = await prisma.review.create({
|
||||||
data: {
|
data: {
|
||||||
user_id: session.user.id,
|
userId: session.user.id,
|
||||||
app_id: appId,
|
appId: appId,
|
||||||
content,
|
content,
|
||||||
rating,
|
rating,
|
||||||
playtime_hours: playtimeHours || null
|
playtimeHours: playtimeHours || null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import NextAuth from 'next-auth'
|
import NextAuth, { NextAuthOptions, getServerSession } from 'next-auth'
|
||||||
import Credentials from 'next-auth/providers/credentials'
|
import CredentialsProvider from 'next-auth/providers/credentials'
|
||||||
import Steam from 'next-auth/providers/steam'
|
|
||||||
import { compare } from 'bcryptjs'
|
import { compare } from 'bcryptjs'
|
||||||
import { prisma } from './db'
|
import { prisma } from './db'
|
||||||
|
import { headers } from 'next/headers'
|
||||||
|
|
||||||
export const { handlers, signIn, signOut, auth } = NextAuth({
|
export { getServerSession }
|
||||||
|
|
||||||
|
export const authOptions: NextAuthOptions = {
|
||||||
|
secret: process.env.NEXTAUTH_SECRET,
|
||||||
providers: [
|
providers: [
|
||||||
Steam({
|
CredentialsProvider({
|
||||||
clientId: process.env.STEAM_CLIENT_ID,
|
|
||||||
clientSecret: process.env.STEAM_CLIENT_SECRET,
|
|
||||||
allowDangerousEmailAccountLinking: true
|
|
||||||
}),
|
|
||||||
Credentials({
|
|
||||||
name: 'credentials',
|
name: 'credentials',
|
||||||
credentials: {
|
credentials: {
|
||||||
email: { label: 'Email', type: 'email' },
|
email: { label: 'Email', type: 'email' },
|
||||||
@@ -54,18 +52,18 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
|||||||
async jwt({ token, user }) {
|
async jwt({ token, user }) {
|
||||||
if (user) {
|
if (user) {
|
||||||
token.id = user.id
|
token.id = user.id
|
||||||
token.steamId = user.steamId
|
token.steamId = (user as any).steamId
|
||||||
token.steamPersonaname = user.steamPersonaname
|
token.steamPersonaname = (user as any).steamPersonaname
|
||||||
token.steamAvatar = user.steamAvatar
|
token.steamAvatar = (user as any).steamAvatar
|
||||||
}
|
}
|
||||||
return token
|
return token
|
||||||
},
|
},
|
||||||
async session({ session, token }) {
|
async session({ session, token }) {
|
||||||
if (token) {
|
if (session.user) {
|
||||||
session.user.id = token.id as string
|
(session.user as any).id = token.id
|
||||||
session.user.steamId = token.steamId as string | null
|
;(session.user as any).steamId = token.steamId
|
||||||
session.user.steamPersonaname = token.steamPersonaname as string | null
|
;(session.user as any).steamPersonaname = token.steamPersonaname
|
||||||
session.user.steamAvatar = token.steamAvatar as string | null
|
;(session.user as any).steamAvatar = token.steamAvatar
|
||||||
}
|
}
|
||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
@@ -76,23 +74,10 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
|||||||
session: {
|
session: {
|
||||||
strategy: 'jwt'
|
strategy: 'jwt'
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
declare module 'next-auth' {
|
|
||||||
interface User {
|
|
||||||
steamId?: string | null
|
|
||||||
steamPersonaname?: string | null
|
|
||||||
steamAvatar?: string | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Session {
|
export async function auth() {
|
||||||
user: {
|
const headersList = await headers()
|
||||||
id: string
|
const session = await getServerSession(authOptions)
|
||||||
email: string
|
return session
|
||||||
name: string
|
|
||||||
steamId?: string | null
|
|
||||||
steamPersonaname?: string | null
|
|
||||||
steamAvatar?: string | null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ export interface SteamGame {
|
|||||||
|
|
||||||
export interface SteamGameDetails {
|
export interface SteamGameDetails {
|
||||||
[appId: string]: {
|
[appId: string]: {
|
||||||
data: {
|
success: boolean
|
||||||
|
data?: {
|
||||||
name: string
|
name: string
|
||||||
short_description: string
|
short_description: string
|
||||||
header_image: string
|
header_image: string
|
||||||
@@ -43,14 +44,14 @@ export interface SteamPlayerStats {
|
|||||||
export async function searchSteamGames(query: string): Promise<SteamGame[]> {
|
export async function searchSteamGames(query: string): Promise<SteamGame[]> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${STEAM_API_BASE}/ISteamGames/GetAppList/v0002/?format=json`
|
`https://api.steampowered.com/IStoreService/GetAppList/v1/?key=${STEAM_API_KEY}`
|
||||||
)
|
)
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
const apps = data.applist?.apps?.app || []
|
const apps = data.response?.apps || []
|
||||||
const filtered = apps
|
const filtered = apps
|
||||||
.filter((app: { appid: number; name: string }) =>
|
.filter((app: { appid: number; name: string }) =>
|
||||||
app.name.toLowerCase().includes(query.toLowerCase())
|
app.name && app.name.toLowerCase().includes(query.toLowerCase())
|
||||||
)
|
)
|
||||||
.slice(0, 20)
|
.slice(0, 20)
|
||||||
|
|
||||||
|
|||||||
12
src/types/next-auth.d.ts
vendored
Normal file
12
src/types/next-auth.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { DefaultSession } from 'next-auth'
|
||||||
|
|
||||||
|
declare module 'next-auth' {
|
||||||
|
interface Session {
|
||||||
|
user: {
|
||||||
|
id: string
|
||||||
|
steamId?: string | null
|
||||||
|
steamPersonaname?: string | null
|
||||||
|
steamAvatar?: string | null
|
||||||
|
} & DefaultSession['user']
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user