feat: add ESLint, Prettier, Husky, and lint-staged setup
This commit is contained in:
24
.commitlintrc.json
Normal file
24
.commitlintrc.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"extends": ["@commitlint/config-conventional"],
|
||||
"rules": {
|
||||
"type-enum": [
|
||||
2,
|
||||
"always",
|
||||
[
|
||||
"feat",
|
||||
"fix",
|
||||
"docs",
|
||||
"style",
|
||||
"refactor",
|
||||
"perf",
|
||||
"test",
|
||||
"build",
|
||||
"ci",
|
||||
"chore",
|
||||
"revert"
|
||||
]
|
||||
],
|
||||
"subject-case": [2, "never", ["upper-case", "pascal-case"]],
|
||||
"header-max-length": [2, "always", 100]
|
||||
}
|
||||
}
|
||||
25
.eslintignore
Normal file
25
.eslintignore
Normal file
@@ -0,0 +1,25 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
**/node_modules/
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
build/
|
||||
out/
|
||||
.next/
|
||||
.nuxt/
|
||||
**/.vitepress/dist/
|
||||
|
||||
# Generated files
|
||||
coverage/
|
||||
*.tsbuildinfo
|
||||
.vscode-test
|
||||
.eslintcache
|
||||
|
||||
# Prisma generated
|
||||
**/src/generated/prisma/
|
||||
|
||||
# Config files that don't need linting
|
||||
*.config.js
|
||||
*.config.ts
|
||||
.prettierrc.js
|
||||
1
.husky/commit-msg
Executable file
1
.husky/commit-msg
Executable file
@@ -0,0 +1 @@
|
||||
npx --no -- commitlint --edit $1
|
||||
1
.husky/pre-commit
Normal file
1
.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
||||
npx lint-staged
|
||||
1
.husky/pre-push
Executable file
1
.husky/pre-push
Executable file
@@ -0,0 +1 @@
|
||||
npm run type-check
|
||||
35
.prettierignore
Normal file
35
.prettierignore
Normal file
@@ -0,0 +1,35 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
**/node_modules/
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
build/
|
||||
out/
|
||||
.next/
|
||||
.nuxt/
|
||||
**/.vitepress/dist/
|
||||
|
||||
# Generated files
|
||||
coverage/
|
||||
*.tsbuildinfo
|
||||
.vscode-test
|
||||
.eslintcache
|
||||
.stylelintcache
|
||||
|
||||
# Prisma generated
|
||||
**/src/generated/prisma/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Lock files
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
10
.prettierrc
Normal file
10
.prettierrc
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"semi": true,
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true,
|
||||
"printWidth": 80,
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
8
.vscode/extensions.json
vendored
Normal file
8
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"prisma.prisma"
|
||||
]
|
||||
}
|
||||
38
.vscode/settings.json
vendored
Normal file
38
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact"
|
||||
],
|
||||
"eslint.workingDirectories": ["./backend", "./frontend"],
|
||||
"files.eol": "\n",
|
||||
"files.insertFinalNewline": true,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
}
|
||||
67
backend/eslint.config.mjs
Normal file
67
backend/eslint.config.mjs
Normal file
@@ -0,0 +1,67 @@
|
||||
// @ts-check
|
||||
import eslint from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import security from 'eslint-plugin-security';
|
||||
import importPlugin from 'eslint-plugin-import';
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: [
|
||||
'node_modules',
|
||||
'dist',
|
||||
'build',
|
||||
'src/generated',
|
||||
'eslint.config.mjs',
|
||||
],
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': tseslint.plugin,
|
||||
security: security,
|
||||
import: importPlugin,
|
||||
},
|
||||
rules: {
|
||||
...security.configs.recommended.rules,
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'@typescript-eslint/explicit-function-return-type': 'warn',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
'no-console': 'warn',
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
groups: [
|
||||
'builtin',
|
||||
'external',
|
||||
'internal',
|
||||
'parent',
|
||||
'sibling',
|
||||
'index',
|
||||
],
|
||||
'newlines-between': 'always',
|
||||
alphabetize: {
|
||||
order: 'asc',
|
||||
caseInsensitive: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
'import/newline-after-import': 'error',
|
||||
'import/no-duplicates': 'error',
|
||||
},
|
||||
}
|
||||
);
|
||||
3094
backend/package-lock.json
generated
3094
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,12 @@
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"dev": "nodemon --watch src --exec ts-node src/index.ts",
|
||||
"dev:api": "nodemon --watch src --exec ts-node --files src/server.ts"
|
||||
"dev:api": "nodemon --watch src --exec ts-node --files src/server.ts",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"lint:fix": "eslint . --ext .ts --fix",
|
||||
"format": "prettier --write \"src/**/*.ts\"",
|
||||
"format:check": "prettier --check \"src/**/*.ts\"",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
@@ -27,6 +32,7 @@
|
||||
"winston": "^3.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.38.0",
|
||||
"@types/cookie-parser": "^1.4.8",
|
||||
"@types/cors": "^2.8.18",
|
||||
"@types/express": "^5.0.2",
|
||||
@@ -35,8 +41,16 @@
|
||||
"@types/morgan": "^1.9.9",
|
||||
"@types/node": "^22.15.21",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.46.1",
|
||||
"@typescript-eslint/parser": "^8.46.1",
|
||||
"eslint": "^9.38.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-security": "^3.0.1",
|
||||
"nodemon": "^3.1.10",
|
||||
"prettier": "^3.6.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.8.3"
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.46.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,11 @@ declare global {
|
||||
|
||||
const ADMIN_DISCORD_IDS = env.ADMIN_DISCORD_IDS;
|
||||
|
||||
export function requireAuth(req: Request, res: Response, next: NextFunction): void {
|
||||
export function requireAuth(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): void {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
|
||||
@@ -50,4 +50,4 @@ export type SuspicionEntry = {
|
||||
|
||||
oldClients?: string[];
|
||||
newClients?: string[];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -89,6 +89,10 @@
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
"noUnusedLocals": true /* Enable error reporting when local variables aren't read. */,
|
||||
"noUnusedParameters": true /* Raise an error when a function parameter isn't read. */,
|
||||
"noImplicitReturns": true /* Enable error reporting for codepaths that do not explicitly return in a function. */,
|
||||
"noFallthroughCasesInSwitch": true /* Enable error reporting for fallthrough cases in switch statements. */,
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
|
||||
@@ -3,26 +3,63 @@ import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import jsxA11y from 'eslint-plugin-jsx-a11y'
|
||||
// @ts-ignore - import plugin doesn't have types
|
||||
import importPlugin from 'eslint-plugin-import'
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
{ ignores: ['dist', 'node_modules'] },
|
||||
{
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
jsxA11y.flatConfigs.recommended,
|
||||
],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
import: importPlugin,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
'no-console': 'warn',
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
groups: [
|
||||
'builtin',
|
||||
'external',
|
||||
'internal',
|
||||
'parent',
|
||||
'sibling',
|
||||
'index',
|
||||
],
|
||||
'newlines-between': 'always',
|
||||
alphabetize: {
|
||||
order: 'asc',
|
||||
caseInsensitive: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
'import/newline-after-import': 'error',
|
||||
'import/no-duplicates': 'error',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
1819
frontend/package-lock.json
generated
1819
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,10 @@
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
|
||||
"format:check": "prettier --check \"src/**/*.{ts,tsx}\"",
|
||||
"type-check": "tsc -b --noEmit",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -26,10 +30,14 @@
|
||||
"@vitejs/plugin-react-swc": "^3.9.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.0.0",
|
||||
"postcss": "^8.5.3",
|
||||
"prettier": "^3.6.2",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.30.1",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import RequireAdmin from './components/RequireAdmin';
|
||||
import RequireAuth from './components/RequireAuth';
|
||||
import { AuthCallback, Bans, Dashboard, Login, Suspicion } from './pages';
|
||||
@@ -16,37 +17,19 @@ function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route
|
||||
path='/login'
|
||||
element={<Login />}
|
||||
/>
|
||||
<Route
|
||||
path='/auth/callback'
|
||||
element={<AuthCallback />}
|
||||
/>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/auth/callback" element={<AuthCallback />} />
|
||||
|
||||
<Route element={<RequireAuth />}>
|
||||
<Route
|
||||
path='/'
|
||||
element={<Dashboard />}
|
||||
/>
|
||||
<Route
|
||||
path='/suspicion'
|
||||
element={<Suspicion />}
|
||||
/>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/suspicion" element={<Suspicion />} />
|
||||
|
||||
<Route element={<RequireAdmin />}>
|
||||
<Route
|
||||
path='/bans'
|
||||
element={<Bans />}
|
||||
/>
|
||||
<Route path="/bans" element={<Bans />} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
path='*'
|
||||
element={<Navigate to='/' />}
|
||||
/>
|
||||
<Route path="*" element={<Navigate to="/" />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import toast from 'react-hot-toast';
|
||||
import { Navigate, Outlet } from 'react-router-dom';
|
||||
|
||||
import { useSession } from '../hooks/useSession';
|
||||
|
||||
export default function RequireAdmin() {
|
||||
@@ -8,12 +9,7 @@ export default function RequireAdmin() {
|
||||
if (loading) return <p>Loading...</p>;
|
||||
if (!session || session.role !== 'ADMIN') {
|
||||
toast.error('You do not have permission to view this page');
|
||||
return (
|
||||
<Navigate
|
||||
to='/'
|
||||
replace
|
||||
/>
|
||||
);
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
return <Outlet />;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import { Navigate, Outlet } from 'react-router-dom';
|
||||
|
||||
import { useSession } from '../hooks/useSession';
|
||||
|
||||
export default function RequireAuth() {
|
||||
const { session, loading } = useSession();
|
||||
|
||||
if (loading) return null; // or <LoadingScreen />
|
||||
return session ? (
|
||||
<Outlet />
|
||||
) : (
|
||||
<Navigate
|
||||
to='/login'
|
||||
replace
|
||||
/>
|
||||
);
|
||||
return session ? <Outlet /> : <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import { useSession } from '../hooks/useSession';
|
||||
import api from '../lib/api';
|
||||
import { useAuth } from '../store/auth';
|
||||
@@ -18,26 +20,26 @@ export default function SessionStatus() {
|
||||
};
|
||||
|
||||
if (loading) return <p>Loading session...</p>;
|
||||
if (error) return <p className='text-red-500'>Error: {error}</p>;
|
||||
if (error) return <p className="text-red-500">Error: {error}</p>;
|
||||
if (!session) return <p>Not logged in</p>;
|
||||
|
||||
return (
|
||||
<div className='flex items-center gap-4 p-4 border rounded bg-gray-900 text-white'>
|
||||
<div className="flex items-center gap-4 p-4 border rounded bg-gray-900 text-white">
|
||||
<img
|
||||
src={`https://cdn.discordapp.com/avatars/${session.discordId}/${session.avatar}.png`}
|
||||
alt='Avatar'
|
||||
className='w-10 h-10 rounded-full'
|
||||
alt="Avatar"
|
||||
className="w-10 h-10 rounded-full"
|
||||
/>
|
||||
<div className='flex-1'>
|
||||
<p className='font-bold'>{session.username}</p>
|
||||
<p className='text-sm text-gray-400'>{session.email}</p>
|
||||
<p className='text-xs mt-1'>
|
||||
Role: <span className='font-mono'>{session.role}</span>
|
||||
<div className="flex-1">
|
||||
<p className="font-bold">{session.username}</p>
|
||||
<p className="text-sm text-gray-400">{session.email}</p>
|
||||
<p className="text-xs mt-1">
|
||||
Role: <span className="font-mono">{session.role}</span>
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className='px-3 py-1 text-sm bg-red-600 rounded hover:bg-red-700'
|
||||
className="px-3 py-1 text-sm bg-red-600 rounded hover:bg-red-700"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import api from '../lib/api';
|
||||
|
||||
type Guild = {
|
||||
id: string;
|
||||
name: string;
|
||||
icon?: string;
|
||||
owner?: boolean;
|
||||
permissions?: string;
|
||||
};
|
||||
|
||||
type Session = {
|
||||
discordId: string;
|
||||
username: string;
|
||||
@@ -9,7 +18,7 @@ type Session = {
|
||||
locale?: string;
|
||||
verified?: boolean;
|
||||
role: 'ADMIN' | 'USER' | 'MODERATOR' | 'BANNED';
|
||||
guilds?: any[];
|
||||
guilds?: Guild[];
|
||||
};
|
||||
|
||||
export function useSession() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios from 'axios';
|
||||
|
||||
import { useAuth } from '../store/auth';
|
||||
|
||||
const api = axios.create({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
|
||||
import App from './App.tsx';
|
||||
import './index.css';
|
||||
|
||||
@@ -7,7 +8,7 @@ createRoot(document.getElementById('root')!).render(
|
||||
<>
|
||||
<App />
|
||||
<Toaster
|
||||
position='top-right'
|
||||
position="top-right"
|
||||
toastOptions={{
|
||||
style: {
|
||||
background: '#1e1e2e', // ctp.base
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import api from '../lib/api';
|
||||
import { useAuth } from '../store/auth';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export default function AuthCallback() {
|
||||
const navigate = useNavigate();
|
||||
@@ -31,5 +32,5 @@ export default function AuthCallback() {
|
||||
});
|
||||
}, [navigate, setToken]);
|
||||
|
||||
return <p className='text-center mt-20'>Logging in with Discord...</p>;
|
||||
return <p className="text-center mt-20">Logging in with Discord...</p>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import api from '../lib/api';
|
||||
|
||||
type UserEntry = {
|
||||
@@ -14,16 +14,21 @@ type UserEntry = {
|
||||
fastReactionCount?: number;
|
||||
};
|
||||
|
||||
type BannedUser = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
function Dashboard() {
|
||||
const [data, setData] = useState<UserEntry[]>([]);
|
||||
const [bannedUsers, setBannedUsers] = useState<Set<string>>(new Set());
|
||||
const [filterBanned, setFilterBanned] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
api.get('/suspicion').then((res) => setData(res.data));
|
||||
api.get('/userbans').then((res) => {
|
||||
const ids = new Set(res.data.map((u: any) => u.userId));
|
||||
const ids = new Set<string>(
|
||||
res.data.map((u: BannedUser) => u.userId)
|
||||
);
|
||||
setBannedUsers(ids);
|
||||
});
|
||||
}, []);
|
||||
@@ -33,21 +38,21 @@ function Dashboard() {
|
||||
: data;
|
||||
|
||||
return (
|
||||
<div className='p-6'>
|
||||
<h1 className='text-2xl font-bold mb-4'>Suspicion Dashboard</h1>
|
||||
<label className='flex items-center gap-2 mb-4'>
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-bold mb-4">Suspicion Dashboard</h1>
|
||||
<label className="flex items-center gap-2 mb-4">
|
||||
<input
|
||||
type='checkbox'
|
||||
type="checkbox"
|
||||
checked={filterBanned}
|
||||
onChange={(e) => setFilterBanned(e.target.checked)}
|
||||
/>
|
||||
Hide banned users
|
||||
</label>
|
||||
|
||||
<table className='min-w-full border'>
|
||||
<thead className='bg-gray-100'>
|
||||
<table className="min-w-full border">
|
||||
<thead className="bg-gray-100">
|
||||
<tr>
|
||||
<th className='text-left px-2 py-1'>Username</th>
|
||||
<th className="text-left px-2 py-1">Username</th>
|
||||
<th>Score</th>
|
||||
<th>Ghost</th>
|
||||
<th>Clients</th>
|
||||
@@ -67,29 +72,29 @@ function Dashboard() {
|
||||
isBanned ? 'bg-red-100 text-red-900' : ''
|
||||
}
|
||||
>
|
||||
<td className='px-2 py-1'>{user.username}</td>
|
||||
<td className='text-center'>
|
||||
<td className="px-2 py-1">{user.username}</td>
|
||||
<td className="text-center">
|
||||
{user.suspicionScore}
|
||||
</td>
|
||||
<td className='text-center'>
|
||||
<td className="text-center">
|
||||
{user.ghostScore}
|
||||
</td>
|
||||
<td className='text-center'>
|
||||
<td className="text-center">
|
||||
{user.multiClientCount}
|
||||
</td>
|
||||
<td className='text-center'>
|
||||
<td className="text-center">
|
||||
{user.channelCount}
|
||||
</td>
|
||||
<td className='text-center'>
|
||||
<td className="text-center">
|
||||
{user.fastReactionCount ?? 0}
|
||||
</td>
|
||||
<td className='text-center'>
|
||||
<td className="text-center">
|
||||
{user.accountAgeDays}
|
||||
</td>
|
||||
<td className='text-center'>
|
||||
<td className="text-center">
|
||||
{isBanned ? (
|
||||
<button
|
||||
className='text-sm text-blue-700 underline'
|
||||
className="text-sm text-blue-700 underline"
|
||||
onClick={() =>
|
||||
api
|
||||
.post('/userunban', {
|
||||
@@ -115,7 +120,7 @@ function Dashboard() {
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className='text-sm text-red-700 underline'
|
||||
className="text-sm text-red-700 underline"
|
||||
onClick={() =>
|
||||
api
|
||||
.post('/userban', {
|
||||
|
||||
@@ -7,10 +7,10 @@ function Login() {
|
||||
const discordLoginUrl = `https://discord.com/oauth2/authorize?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code&scope=identify+guilds`;
|
||||
|
||||
return (
|
||||
<div className='flex items-center justify-center h-screen'>
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<a
|
||||
href={discordLoginUrl}
|
||||
className='bg-indigo-600 text-white px-6 py-3 rounded-md text-lg font-semibold'
|
||||
className="bg-indigo-600 text-white px-6 py-3 rounded-md text-lg font-semibold"
|
||||
>
|
||||
Login with Discord
|
||||
</a>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
|
||||
2056
package-lock.json
generated
Normal file
2056
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
package.json
Normal file
33
package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "discord-spywatcher",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Discord Spywatcher - Backend and Frontend monorepo",
|
||||
"scripts": {
|
||||
"prepare": "husky",
|
||||
"lint": "npm run lint --prefix backend && npm run lint --prefix frontend",
|
||||
"lint:fix": "npm run lint:fix --prefix backend && npm run lint:fix --prefix frontend",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
|
||||
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\"",
|
||||
"type-check": "npm run type-check --prefix backend && npm run type-check --prefix frontend"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^19.0.0",
|
||||
"@commitlint/config-conventional": "^19.0.0",
|
||||
"husky": "^9.0.0",
|
||||
"lint-staged": "^15.0.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
"backend/**/*.ts": [
|
||||
"npx eslint --fix",
|
||||
"npx prettier --write"
|
||||
],
|
||||
"frontend/**/*.{ts,tsx}": [
|
||||
"npx eslint --fix",
|
||||
"npx prettier --write"
|
||||
],
|
||||
"*.{json,md}": [
|
||||
"npx prettier --write"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user