refactor(auth): remove debug logging from authentication processes
- Eliminated extensive console logging in the auth controller, deserialize, serialize, and passport strategies to streamline code and improve performance. - Simplified response handling in the auth controller by directly returning the AuthResponse object. - Updated session middleware to enhance clarity and maintainability by removing unnecessary debug functions and logs.
This commit is contained in:
@@ -28,58 +28,21 @@ export default class AuthController extends WorklenzControllerBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static verify(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
public static verify(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
||||||
console.log("=== VERIFY DEBUG ===");
|
|
||||||
console.log("req.user:", req.user);
|
|
||||||
console.log("req.isAuthenticated():", req.isAuthenticated());
|
|
||||||
console.log("req.session.passport:", (req.session as any).passport);
|
|
||||||
console.log("req.session.id:", req.sessionID);
|
|
||||||
console.log("Full session object:", JSON.stringify(req.session, null, 2));
|
|
||||||
console.log("req.query.strategy:", req.query.strategy);
|
|
||||||
|
|
||||||
// Check if session exists in database
|
|
||||||
if (req.sessionID) {
|
|
||||||
db.query("SELECT sid, sess FROM pg_sessions WHERE sid = $1", [req.sessionID])
|
|
||||||
.then(result => {
|
|
||||||
if (result.rows.length > 0) {
|
|
||||||
console.log("Session found in database:");
|
|
||||||
console.log("Session ID:", result.rows[0].sid);
|
|
||||||
console.log("Session data:", JSON.stringify(result.rows[0].sess, null, 2));
|
|
||||||
} else {
|
|
||||||
console.log("Session NOT FOUND in database for ID:", req.sessionID);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.log("Error checking session in database:", err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flash messages sent from passport-local-signup.ts and passport-local-login.ts
|
// Flash messages sent from passport-local-signup.ts and passport-local-login.ts
|
||||||
const errors = req.flash()["error"] || [];
|
const errors = req.flash()["error"] || [];
|
||||||
const messages = req.flash()["success"] || [];
|
const messages = req.flash()["success"] || [];
|
||||||
|
|
||||||
console.log("Flash errors:", errors);
|
|
||||||
console.log("Flash messages:", messages);
|
|
||||||
|
|
||||||
// If there are multiple messages, we will send one at a time.
|
// If there are multiple messages, we will send one at a time.
|
||||||
const auth_error = errors.length > 0 ? errors[0] : null;
|
const auth_error = errors.length > 0 ? errors[0] : null;
|
||||||
const message = messages.length > 0 ? messages[0] : null;
|
const message = messages.length > 0 ? messages[0] : null;
|
||||||
|
|
||||||
const midTitle = req.query.strategy === "login" ? "Login Failed!" : "Signup Failed!";
|
const midTitle = req.query.strategy === "login" ? "Login Failed!" : "Signup Failed!";
|
||||||
const title = req.query.strategy ? midTitle : null;
|
const title = req.query.strategy ? midTitle : null;
|
||||||
|
|
||||||
console.log("Title:", title);
|
|
||||||
console.log("Auth error:", auth_error);
|
|
||||||
console.log("Success message:", message);
|
|
||||||
console.log("Is authenticated:", req.isAuthenticated());
|
|
||||||
console.log("Has user:", !!req.user);
|
|
||||||
|
|
||||||
if (req.user)
|
if (req.user)
|
||||||
req.user.build_v = FileConstants.getRelease();
|
req.user.build_v = FileConstants.getRelease();
|
||||||
|
|
||||||
const response = new AuthResponse(title, req.isAuthenticated(), req.user || null, auth_error, message);
|
return res.status(200).send(new AuthResponse(title, req.isAuthenticated(), req.user || null, auth_error, message));
|
||||||
console.log("Sending response:", response);
|
|
||||||
|
|
||||||
return res.status(200).send(response);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static logout(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
public static logout(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
||||||
|
|||||||
@@ -5,123 +5,6 @@ import { isProduction } from "../shared/utils";
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const pgSession = require("connect-pg-simple")(session);
|
const pgSession = require("connect-pg-simple")(session);
|
||||||
|
|
||||||
// Test database connection and pg_sessions table
|
|
||||||
async function testSessionStore() {
|
|
||||||
try {
|
|
||||||
console.log("=== SESSION STORE DEBUG ===");
|
|
||||||
|
|
||||||
// Test basic database connection
|
|
||||||
const testQuery = await db.query("SELECT NOW() as current_time");
|
|
||||||
console.log("Database connection test:", testQuery.rows[0]);
|
|
||||||
|
|
||||||
// Check if pg_sessions table exists
|
|
||||||
const tableCheck = await db.query(`
|
|
||||||
SELECT EXISTS (
|
|
||||||
SELECT FROM information_schema.tables
|
|
||||||
WHERE table_name = 'pg_sessions'
|
|
||||||
) as table_exists
|
|
||||||
`);
|
|
||||||
console.log("pg_sessions table exists:", tableCheck.rows[0].table_exists);
|
|
||||||
|
|
||||||
if (tableCheck.rows[0].table_exists) {
|
|
||||||
// Check table structure
|
|
||||||
const structureQuery = await db.query(`
|
|
||||||
SELECT column_name, data_type
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_name = 'pg_sessions'
|
|
||||||
ORDER BY ordinal_position
|
|
||||||
`);
|
|
||||||
console.log("pg_sessions table structure:", structureQuery.rows);
|
|
||||||
|
|
||||||
// Check current sessions count
|
|
||||||
const countQuery = await db.query("SELECT COUNT(*) as session_count FROM pg_sessions");
|
|
||||||
console.log("Current sessions in database:", countQuery.rows[0].session_count);
|
|
||||||
|
|
||||||
// Check recent sessions
|
|
||||||
const recentQuery = await db.query(`
|
|
||||||
SELECT sid, expire
|
|
||||||
FROM pg_sessions
|
|
||||||
ORDER BY expire DESC
|
|
||||||
LIMIT 3
|
|
||||||
`);
|
|
||||||
console.log("Recent sessions:", recentQuery.rows);
|
|
||||||
} else {
|
|
||||||
console.log("ERROR: pg_sessions table does not exist!");
|
|
||||||
|
|
||||||
// Try to create the table
|
|
||||||
console.log("Attempting to create pg_sessions table...");
|
|
||||||
await db.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS pg_sessions (
|
|
||||||
sid VARCHAR NOT NULL COLLATE "default",
|
|
||||||
sess JSON NOT NULL,
|
|
||||||
expire TIMESTAMP(6) NOT NULL
|
|
||||||
)
|
|
||||||
WITH (OIDS=FALSE);
|
|
||||||
|
|
||||||
ALTER TABLE pg_sessions ADD CONSTRAINT session_pkey PRIMARY KEY (sid) NOT DEFERRABLE INITIALLY IMMEDIATE;
|
|
||||||
CREATE INDEX IF NOT EXISTS IDX_session_expire ON pg_sessions (expire);
|
|
||||||
`);
|
|
||||||
console.log("pg_sessions table created successfully");
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("=== END SESSION STORE DEBUG ===");
|
|
||||||
} catch (error) {
|
|
||||||
console.log("Session store test error:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the test
|
|
||||||
testSessionStore();
|
|
||||||
|
|
||||||
const store = new pgSession({
|
|
||||||
pool: db.pool,
|
|
||||||
tableName: "pg_sessions"
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add store event listeners
|
|
||||||
store.on("connect", () => {
|
|
||||||
console.log("Session store connected to database");
|
|
||||||
});
|
|
||||||
|
|
||||||
store.on("disconnect", () => {
|
|
||||||
console.log("Session store disconnected from database");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Override store methods to add debugging
|
|
||||||
const originalSet = store.set.bind(store);
|
|
||||||
const originalGet = store.get.bind(store);
|
|
||||||
|
|
||||||
store.set = function(sid: string, session: any, callback: any) {
|
|
||||||
console.log(`=== SESSION SET ===`);
|
|
||||||
console.log(`Session ID: ${sid}`);
|
|
||||||
console.log(`Session data:`, JSON.stringify(session, null, 2));
|
|
||||||
|
|
||||||
return originalSet(sid, session, (err: any) => {
|
|
||||||
if (err) {
|
|
||||||
console.log(`Session SET ERROR for ${sid}:`, err);
|
|
||||||
} else {
|
|
||||||
console.log(`Session SET SUCCESS for ${sid}`);
|
|
||||||
}
|
|
||||||
callback && callback(err);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
store.get = function(sid: string, callback: any) {
|
|
||||||
console.log(`=== SESSION GET ===`);
|
|
||||||
console.log(`Requesting session ID: ${sid}`);
|
|
||||||
|
|
||||||
return originalGet(sid, (err: any, session: any) => {
|
|
||||||
if (err) {
|
|
||||||
console.log(`Session GET ERROR for ${sid}:`, err);
|
|
||||||
} else if (session) {
|
|
||||||
console.log(`Session GET SUCCESS for ${sid}:`, JSON.stringify(session, null, 2));
|
|
||||||
} else {
|
|
||||||
console.log(`Session GET: No session found for ${sid}`);
|
|
||||||
}
|
|
||||||
callback(err, session);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default session({
|
export default session({
|
||||||
name: process.env.SESSION_NAME,
|
name: process.env.SESSION_NAME,
|
||||||
secret: process.env.SESSION_SECRET || "development-secret-key",
|
secret: process.env.SESSION_SECRET || "development-secret-key",
|
||||||
@@ -129,13 +12,14 @@ export default session({
|
|||||||
resave: true,
|
resave: true,
|
||||||
saveUninitialized: false,
|
saveUninitialized: false,
|
||||||
rolling: true,
|
rolling: true,
|
||||||
store,
|
store: new pgSession({
|
||||||
|
pool: db.pool,
|
||||||
|
tableName: "pg_sessions"
|
||||||
|
}),
|
||||||
cookie: {
|
cookie: {
|
||||||
path: "/",
|
path: "/",
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
// sameSite: "none",
|
|
||||||
// domain: isProduction() ? ".worklenz.com" : undefined,
|
|
||||||
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
|
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -22,42 +22,18 @@ async function clearEmailInvitations(email: string, teamId: string) {
|
|||||||
// Check whether the user still exists on the database
|
// Check whether the user still exists on the database
|
||||||
export async function deserialize(user: { id: string | null }, done: IDeserializeCallback) {
|
export async function deserialize(user: { id: string | null }, done: IDeserializeCallback) {
|
||||||
try {
|
try {
|
||||||
console.log("=== DESERIALIZE DEBUG ===");
|
|
||||||
console.log("User object:", user);
|
|
||||||
|
|
||||||
if (!user || !user.id) {
|
if (!user || !user.id) {
|
||||||
console.log("No user or user.id, returning null");
|
|
||||||
return done(null, null);
|
return done(null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const {id} = user;
|
const {id} = user;
|
||||||
console.log("Deserializing user ID:", id);
|
|
||||||
|
|
||||||
// First check if user exists in users table
|
|
||||||
const userCheck = await db.query("SELECT id, active_team FROM users WHERE id = $1", [id]);
|
|
||||||
console.log("User exists check:", userCheck.rowCount, userCheck.rows[0]);
|
|
||||||
|
|
||||||
if (!userCheck.rowCount) {
|
|
||||||
console.log("User not found in users table");
|
|
||||||
return done(null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
const excludedSubscriptionTypes = ["TRIAL", "PADDLE"];
|
const excludedSubscriptionTypes = ["TRIAL", "PADDLE"];
|
||||||
const q = `SELECT deserialize_user($1) AS user;`;
|
const q = `SELECT deserialize_user($1) AS user;`;
|
||||||
console.log("Calling deserialize_user with ID:", id);
|
|
||||||
|
|
||||||
const result = await db.query(q, [id]);
|
const result = await db.query(q, [id]);
|
||||||
|
|
||||||
console.log("Database query result rows length:", result.rows.length);
|
|
||||||
console.log("Raw database result:", result.rows);
|
|
||||||
|
|
||||||
if (result.rows.length) {
|
if (result.rows.length) {
|
||||||
const [data] = result.rows;
|
const [data] = result.rows;
|
||||||
console.log("Database result data:", data);
|
|
||||||
|
|
||||||
if (data?.user) {
|
if (data?.user) {
|
||||||
console.log("User data found:", data.user);
|
|
||||||
|
|
||||||
const realExpiredDate = moment(data.user.valid_till_date).add(7, "days");
|
const realExpiredDate = moment(data.user.valid_till_date).add(7, "days");
|
||||||
data.user.is_expired = false;
|
data.user.is_expired = false;
|
||||||
|
|
||||||
@@ -67,17 +43,11 @@ export async function deserialize(user: { id: string | null }, done: IDeserializ
|
|||||||
void setLastActive(data.user.id);
|
void setLastActive(data.user.id);
|
||||||
void clearEmailInvitations(data.user.email, data.user.team_id);
|
void clearEmailInvitations(data.user.email, data.user.team_id);
|
||||||
|
|
||||||
console.log("Returning successful user:", data.user);
|
|
||||||
return done(null, data.user as IPassportSession);
|
return done(null, data.user as IPassportSession);
|
||||||
}
|
}
|
||||||
console.log("No user data in result - deserialize_user returned null");
|
|
||||||
}
|
}
|
||||||
console.log("No rows returned from database");
|
|
||||||
|
|
||||||
console.log("Returning null user");
|
|
||||||
return done(null, null);
|
return done(null, null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Deserialize error:", error);
|
|
||||||
return done(error, null);
|
return done(error, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,7 @@ import db from "../../config/db";
|
|||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
|
|
||||||
async function handleLogin(req: Request, email: string, password: string, done: any) {
|
async function handleLogin(req: Request, email: string, password: string, done: any) {
|
||||||
console.log("Login attempt for:", email);
|
|
||||||
|
|
||||||
if (!email || !password) {
|
if (!email || !password) {
|
||||||
console.log("Missing credentials");
|
|
||||||
return done(null, false, { message: "Please enter both email and password" });
|
return done(null, false, { message: "Please enter both email and password" });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,29 +16,21 @@ async function handleLogin(req: Request, email: string, password: string, done:
|
|||||||
AND google_id IS NULL
|
AND google_id IS NULL
|
||||||
AND is_deleted IS FALSE;`;
|
AND is_deleted IS FALSE;`;
|
||||||
const result = await db.query(q, [email]);
|
const result = await db.query(q, [email]);
|
||||||
console.log("User query result count:", result.rowCount);
|
|
||||||
|
|
||||||
const [data] = result.rows;
|
const [data] = result.rows;
|
||||||
console.log("data", data);
|
|
||||||
|
|
||||||
if (!data?.password) {
|
if (!data?.password) {
|
||||||
console.log("No account found");
|
|
||||||
return done(null, false, { message: "No account found with this email" });
|
return done(null, false, { message: "No account found with this email" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const passwordMatch = bcrypt.compareSync(password, data.password);
|
const passwordMatch = bcrypt.compareSync(password, data.password);
|
||||||
console.log("Password match:", passwordMatch);
|
|
||||||
|
|
||||||
if (passwordMatch && email === data.email) {
|
if (passwordMatch && email === data.email) {
|
||||||
delete data.password;
|
delete data.password;
|
||||||
console.log("=== LOGIN SUCCESS DEBUG ===");
|
|
||||||
console.log("About to call done with user:", data);
|
|
||||||
console.log("User structure:", JSON.stringify(data, null, 2));
|
|
||||||
return done(null, data, {message: "User successfully logged in"});
|
return done(null, data, {message: "User successfully logged in"});
|
||||||
}
|
}
|
||||||
return done(null, false, { message: "Incorrect email or password" });
|
return done(null, false, { message: "Incorrect email or password" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Login error:", error);
|
|
||||||
log_error(error, req.body);
|
log_error(error, req.body);
|
||||||
return done(error);
|
return done(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,15 +37,8 @@ async function registerUser(password: string, team_id: string, name: string, tea
|
|||||||
team_member_id,
|
team_member_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("=== REGISTER USER DEBUG ===");
|
|
||||||
console.log("Calling register_user with body:", body);
|
|
||||||
|
|
||||||
const result = await db.query(q, [JSON.stringify(body)]);
|
const result = await db.query(q, [JSON.stringify(body)]);
|
||||||
const [data] = result.rows;
|
const [data] = result.rows;
|
||||||
|
|
||||||
console.log("Register user result:", data);
|
|
||||||
console.log("User object returned:", data.user);
|
|
||||||
|
|
||||||
return data.user;
|
return data.user;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,22 +59,11 @@ async function handleSignUp(req: Request, email: string, password: string, done:
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log("=== SIGNUP DEBUG ===");
|
|
||||||
console.log("About to register user with data:", {name, team_name, email, timezone, team_member_id, team_id});
|
|
||||||
|
|
||||||
const user = await registerUser(password, team_id, name, team_name, email, timezone, team_member_id);
|
const user = await registerUser(password, team_id, name, team_name, email, timezone, team_member_id);
|
||||||
|
|
||||||
console.log("User registration successful, user object:", user);
|
|
||||||
|
|
||||||
sendWelcomeEmail(email, name);
|
sendWelcomeEmail(email, name);
|
||||||
|
|
||||||
console.log("About to call done with user:", user);
|
|
||||||
req.flash(SUCCESS_KEY, "Registration successful. Please check your email for verification.");
|
req.flash(SUCCESS_KEY, "Registration successful. Please check your email for verification.");
|
||||||
return done(null, user, {message: "Registration successful. Please check your email for verification."});
|
return done(null, user, {message: "Registration successful. Please check your email for verification."});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.log("=== SIGNUP ERROR ===");
|
|
||||||
console.log("Error during signup:", error);
|
|
||||||
|
|
||||||
const message = (error?.message) || "";
|
const message = (error?.message) || "";
|
||||||
|
|
||||||
if (message === "ERROR_INVALID_JOINING_EMAIL") {
|
if (message === "ERROR_INVALID_JOINING_EMAIL") {
|
||||||
|
|||||||
@@ -3,14 +3,5 @@ import {IPassportSession} from "../interfaces/passport-session";
|
|||||||
|
|
||||||
// Parse the user id to deserialize function
|
// Parse the user id to deserialize function
|
||||||
export function serialize($user: IPassportSession, done: ISerializeCallback) {
|
export function serialize($user: IPassportSession, done: ISerializeCallback) {
|
||||||
console.log("=== SERIALIZE DEBUG ===");
|
done(null, { id: $user?.id ?? null });
|
||||||
console.log("Serializing user:", $user);
|
|
||||||
console.log("User ID:", $user?.id);
|
|
||||||
|
|
||||||
const serializedUser = { id: $user?.id ?? null };
|
|
||||||
console.log("Serialized user object:", serializedUser);
|
|
||||||
|
|
||||||
done(null, serializedUser);
|
|
||||||
|
|
||||||
console.log("Serialize done callback completed");
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,7 @@ import { colors } from '../styles/colors';
|
|||||||
import { verifyAuthentication } from '@/features/auth/authSlice';
|
import { verifyAuthentication } from '@/features/auth/authSlice';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
import HubSpot from '@/components/HubSpot';
|
|
||||||
import LicenseAlert from '@/components/license-alert';
|
|
||||||
import { useAuthService } from '@/hooks/useAuth';
|
import { useAuthService } from '@/hooks/useAuth';
|
||||||
import { ILocalSession } from '@/types/auth/local-session.types';
|
|
||||||
|
|
||||||
const MainLayout = () => {
|
const MainLayout = () => {
|
||||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||||
@@ -33,18 +30,6 @@ const MainLayout = () => {
|
|||||||
void verifyAuthStatus();
|
void verifyAuthStatus();
|
||||||
}, [dispatch, navigate]);
|
}, [dispatch, navigate]);
|
||||||
|
|
||||||
const handleUpgrade = () => {
|
|
||||||
// Handle upgrade logic here
|
|
||||||
console.log('Upgrade clicked');
|
|
||||||
// You can navigate to upgrade page or open a modal
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleExtend = () => {
|
|
||||||
// Handle license extension logic here
|
|
||||||
console.log('Extend license clicked');
|
|
||||||
// You can navigate to renewal page or open a modal
|
|
||||||
};
|
|
||||||
|
|
||||||
const alertHeight = showAlert ? 64 : 0; // Fixed height for license alert
|
const alertHeight = showAlert ? 64 : 0; // Fixed height for license alert
|
||||||
|
|
||||||
const headerStyles = {
|
const headerStyles = {
|
||||||
@@ -59,7 +44,7 @@ const MainLayout = () => {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const contentStyles = {
|
const contentStyles = {
|
||||||
paddingInline: isDesktop ? 64 : 24,
|
paddingInline: isDesktop ? 0 : 24,
|
||||||
overflowX: 'hidden',
|
overflowX: 'hidden',
|
||||||
marginTop: alertHeight + 64, // Adjust top margin based on alert height + navbar height
|
marginTop: alertHeight + 64, // Adjust top margin based on alert height + navbar height
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ const ProjectView = () => {
|
|||||||
), []);
|
), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ marginBlockStart: 80, marginBlockEnd: 24, minHeight: '80vh' }}>
|
<div style={{ marginBlockStart: 15, marginBlockEnd: 24, minHeight: '80vh' }}>
|
||||||
<ProjectViewHeader />
|
<ProjectViewHeader />
|
||||||
|
|
||||||
<Tabs
|
<Tabs
|
||||||
|
|||||||
Reference in New Issue
Block a user