chore: Update dependencies, refactor ESLint config, and enhance test infrastructure
This commit is contained in:
parent
64ba7cfc31
commit
9c7e33499a
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -10,6 +10,7 @@ lerna-debug.log*
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
|
coverage
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
# Environment variables
|
# Environment variables
|
||||||
|
|
|
||||||
|
|
@ -3,21 +3,34 @@ import globals from 'globals'
|
||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
import tseslint from 'typescript-eslint'
|
import tseslint from 'typescript-eslint'
|
||||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
||||||
|
|
||||||
export default defineConfig([
|
export default tseslint.config(
|
||||||
globalIgnores(['dist']),
|
{ ignores: ['dist'] },
|
||||||
{
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
files: ['**/*.{ts,tsx}'],
|
files: ['**/*.{ts,tsx}'],
|
||||||
extends: [
|
|
||||||
js.configs.recommended,
|
|
||||||
tseslint.configs.recommended,
|
|
||||||
reactHooks.configs.flat.recommended,
|
|
||||||
reactRefresh.configs.vite,
|
|
||||||
],
|
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
ecmaVersion: 2020,
|
ecmaVersion: 2020,
|
||||||
globals: globals.browser,
|
globals: globals.browser,
|
||||||
},
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
'@typescript-eslint/no-explicit-any': 'warn', // Change from error to warning
|
||||||
|
},
|
||||||
},
|
},
|
||||||
])
|
{
|
||||||
|
files: ['src/test/**/*.{ts,tsx}'],
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off', // Allow any in test files
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/admin-icon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
<title>yaltopia-ticket-admin</title>
|
<title>Yaltopia Ticket Admin</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
||||||
384
package-lock.json
generated
384
package-lock.json
generated
|
|
@ -21,7 +21,7 @@
|
||||||
"@radix-ui/react-toast": "^1.2.15",
|
"@radix-ui/react-toast": "^1.2.15",
|
||||||
"@sentry/react": "^10.39.0",
|
"@sentry/react": "^10.39.0",
|
||||||
"@tanstack/react-query": "^5.90.12",
|
"@tanstack/react-query": "^5.90.12",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.5",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
|
@ -42,6 +42,7 @@
|
||||||
"@types/react": "^19.2.5",
|
"@types/react": "^19.2.5",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^5.1.1",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
|
"@vitest/coverage-v8": "^4.0.18",
|
||||||
"@vitest/ui": "^4.0.18",
|
"@vitest/ui": "^4.0.18",
|
||||||
"autoprefixer": "^10.4.23",
|
"autoprefixer": "^10.4.23",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
|
|
@ -435,6 +436,16 @@
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@bcoe/v8-coverage": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@bramus/specificity": {
|
"node_modules/@bramus/specificity": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
|
||||||
|
|
@ -1106,20 +1117,20 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/eslintrc": {
|
"node_modules/@eslint/eslintrc": {
|
||||||
"version": "3.3.3",
|
"version": "3.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz",
|
||||||
"integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==",
|
"integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajv": "^6.12.4",
|
"ajv": "^6.14.0",
|
||||||
"debug": "^4.3.2",
|
"debug": "^4.3.2",
|
||||||
"espree": "^10.0.1",
|
"espree": "^10.0.1",
|
||||||
"globals": "^14.0.0",
|
"globals": "^14.0.0",
|
||||||
"ignore": "^5.2.0",
|
"ignore": "^5.2.0",
|
||||||
"import-fresh": "^3.2.1",
|
"import-fresh": "^3.2.1",
|
||||||
"js-yaml": "^4.1.1",
|
"js-yaml": "^4.1.1",
|
||||||
"minimatch": "^3.1.2",
|
"minimatch": "^3.1.3",
|
||||||
"strip-json-comments": "^3.1.1"
|
"strip-json-comments": "^3.1.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -1143,9 +1154,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/js": {
|
"node_modules/@eslint/js": {
|
||||||
"version": "9.39.2",
|
"version": "9.39.3",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz",
|
||||||
"integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
|
"integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -3871,17 +3882,17 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.54.0",
|
"version": "8.56.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz",
|
||||||
"integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==",
|
"integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/regexpp": "^4.12.2",
|
"@eslint-community/regexpp": "^4.12.2",
|
||||||
"@typescript-eslint/scope-manager": "8.54.0",
|
"@typescript-eslint/scope-manager": "8.56.1",
|
||||||
"@typescript-eslint/type-utils": "8.54.0",
|
"@typescript-eslint/type-utils": "8.56.1",
|
||||||
"@typescript-eslint/utils": "8.54.0",
|
"@typescript-eslint/utils": "8.56.1",
|
||||||
"@typescript-eslint/visitor-keys": "8.54.0",
|
"@typescript-eslint/visitor-keys": "8.56.1",
|
||||||
"ignore": "^7.0.5",
|
"ignore": "^7.0.5",
|
||||||
"natural-compare": "^1.4.0",
|
"natural-compare": "^1.4.0",
|
||||||
"ts-api-utils": "^2.4.0"
|
"ts-api-utils": "^2.4.0"
|
||||||
|
|
@ -3894,8 +3905,8 @@
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@typescript-eslint/parser": "^8.54.0",
|
"@typescript-eslint/parser": "^8.56.1",
|
||||||
"eslint": "^8.57.0 || ^9.0.0",
|
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||||
"typescript": ">=4.8.4 <6.0.0"
|
"typescript": ">=4.8.4 <6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -3910,16 +3921,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "8.54.0",
|
"version": "8.56.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz",
|
||||||
"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
|
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.54.0",
|
"@typescript-eslint/scope-manager": "8.56.1",
|
||||||
"@typescript-eslint/types": "8.54.0",
|
"@typescript-eslint/types": "8.56.1",
|
||||||
"@typescript-eslint/typescript-estree": "8.54.0",
|
"@typescript-eslint/typescript-estree": "8.56.1",
|
||||||
"@typescript-eslint/visitor-keys": "8.54.0",
|
"@typescript-eslint/visitor-keys": "8.56.1",
|
||||||
"debug": "^4.4.3"
|
"debug": "^4.4.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -3930,19 +3941,19 @@
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": "^8.57.0 || ^9.0.0",
|
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||||
"typescript": ">=4.8.4 <6.0.0"
|
"typescript": ">=4.8.4 <6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/project-service": {
|
"node_modules/@typescript-eslint/project-service": {
|
||||||
"version": "8.54.0",
|
"version": "8.56.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz",
|
||||||
"integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==",
|
"integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/tsconfig-utils": "^8.54.0",
|
"@typescript-eslint/tsconfig-utils": "^8.56.1",
|
||||||
"@typescript-eslint/types": "^8.54.0",
|
"@typescript-eslint/types": "^8.56.1",
|
||||||
"debug": "^4.4.3"
|
"debug": "^4.4.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -3957,14 +3968,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/scope-manager": {
|
"node_modules/@typescript-eslint/scope-manager": {
|
||||||
"version": "8.54.0",
|
"version": "8.56.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz",
|
||||||
"integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==",
|
"integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.54.0",
|
"@typescript-eslint/types": "8.56.1",
|
||||||
"@typescript-eslint/visitor-keys": "8.54.0"
|
"@typescript-eslint/visitor-keys": "8.56.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|
@ -3975,9 +3986,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||||
"version": "8.54.0",
|
"version": "8.56.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz",
|
||||||
"integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==",
|
"integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -3992,15 +4003,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/type-utils": {
|
"node_modules/@typescript-eslint/type-utils": {
|
||||||
"version": "8.54.0",
|
"version": "8.56.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz",
|
||||||
"integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==",
|
"integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.54.0",
|
"@typescript-eslint/types": "8.56.1",
|
||||||
"@typescript-eslint/typescript-estree": "8.54.0",
|
"@typescript-eslint/typescript-estree": "8.56.1",
|
||||||
"@typescript-eslint/utils": "8.54.0",
|
"@typescript-eslint/utils": "8.56.1",
|
||||||
"debug": "^4.4.3",
|
"debug": "^4.4.3",
|
||||||
"ts-api-utils": "^2.4.0"
|
"ts-api-utils": "^2.4.0"
|
||||||
},
|
},
|
||||||
|
|
@ -4012,14 +4023,14 @@
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": "^8.57.0 || ^9.0.0",
|
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||||
"typescript": ">=4.8.4 <6.0.0"
|
"typescript": ">=4.8.4 <6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/types": {
|
"node_modules/@typescript-eslint/types": {
|
||||||
"version": "8.54.0",
|
"version": "8.56.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz",
|
||||||
"integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==",
|
"integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -4031,18 +4042,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree": {
|
"node_modules/@typescript-eslint/typescript-estree": {
|
||||||
"version": "8.54.0",
|
"version": "8.56.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz",
|
||||||
"integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==",
|
"integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/project-service": "8.54.0",
|
"@typescript-eslint/project-service": "8.56.1",
|
||||||
"@typescript-eslint/tsconfig-utils": "8.54.0",
|
"@typescript-eslint/tsconfig-utils": "8.56.1",
|
||||||
"@typescript-eslint/types": "8.54.0",
|
"@typescript-eslint/types": "8.56.1",
|
||||||
"@typescript-eslint/visitor-keys": "8.54.0",
|
"@typescript-eslint/visitor-keys": "8.56.1",
|
||||||
"debug": "^4.4.3",
|
"debug": "^4.4.3",
|
||||||
"minimatch": "^9.0.5",
|
"minimatch": "^10.2.2",
|
||||||
"semver": "^7.7.3",
|
"semver": "^7.7.3",
|
||||||
"tinyglobby": "^0.2.15",
|
"tinyglobby": "^0.2.15",
|
||||||
"ts-api-utils": "^2.4.0"
|
"ts-api-utils": "^2.4.0"
|
||||||
|
|
@ -4058,36 +4069,49 @@
|
||||||
"typescript": ">=4.8.4 <6.0.0"
|
"typescript": ">=4.8.4 <6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||||
"version": "2.0.2",
|
"version": "5.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
|
||||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^4.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
|
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
|
||||||
"version": "9.0.5",
|
"version": "10.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz",
|
||||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
"integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "BlueOak-1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"brace-expansion": "^2.0.1"
|
"brace-expansion": "^5.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16 || 14 >=14.17"
|
"node": "18 || 20 || >=22"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
|
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
|
||||||
"version": "7.7.3",
|
"version": "7.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -4098,16 +4122,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/utils": {
|
"node_modules/@typescript-eslint/utils": {
|
||||||
"version": "8.54.0",
|
"version": "8.56.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz",
|
||||||
"integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==",
|
"integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.9.1",
|
"@eslint-community/eslint-utils": "^4.9.1",
|
||||||
"@typescript-eslint/scope-manager": "8.54.0",
|
"@typescript-eslint/scope-manager": "8.56.1",
|
||||||
"@typescript-eslint/types": "8.54.0",
|
"@typescript-eslint/types": "8.56.1",
|
||||||
"@typescript-eslint/typescript-estree": "8.54.0"
|
"@typescript-eslint/typescript-estree": "8.56.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|
@ -4117,19 +4141,19 @@
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": "^8.57.0 || ^9.0.0",
|
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||||
"typescript": ">=4.8.4 <6.0.0"
|
"typescript": ">=4.8.4 <6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/visitor-keys": {
|
"node_modules/@typescript-eslint/visitor-keys": {
|
||||||
"version": "8.54.0",
|
"version": "8.56.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz",
|
||||||
"integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==",
|
"integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.54.0",
|
"@typescript-eslint/types": "8.56.1",
|
||||||
"eslint-visitor-keys": "^4.2.1"
|
"eslint-visitor-keys": "^5.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|
@ -4139,6 +4163,19 @@
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/eslint"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vitejs/plugin-react": {
|
"node_modules/@vitejs/plugin-react": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz",
|
||||||
|
|
@ -4160,6 +4197,37 @@
|
||||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vitest/coverage-v8": {
|
||||||
|
"version": "4.0.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz",
|
||||||
|
"integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@bcoe/v8-coverage": "^1.0.2",
|
||||||
|
"@vitest/utils": "4.0.18",
|
||||||
|
"ast-v8-to-istanbul": "^0.3.10",
|
||||||
|
"istanbul-lib-coverage": "^3.2.2",
|
||||||
|
"istanbul-lib-report": "^3.0.1",
|
||||||
|
"istanbul-reports": "^3.2.0",
|
||||||
|
"magicast": "^0.5.1",
|
||||||
|
"obug": "^2.1.1",
|
||||||
|
"std-env": "^3.10.0",
|
||||||
|
"tinyrainbow": "^3.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vitest/browser": "4.0.18",
|
||||||
|
"vitest": "4.0.18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vitest/browser": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vitest/expect": {
|
"node_modules/@vitest/expect": {
|
||||||
"version": "4.0.18",
|
"version": "4.0.18",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz",
|
||||||
|
|
@ -4327,9 +4395,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.12.6",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
||||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -4437,6 +4505,25 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ast-v8-to-istanbul": {
|
||||||
|
"version": "0.3.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.11.tgz",
|
||||||
|
"integrity": "sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/trace-mapping": "^0.3.31",
|
||||||
|
"estree-walker": "^3.0.3",
|
||||||
|
"js-tokens": "^10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ast-v8-to-istanbul/node_modules/js-tokens": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/asynckit": {
|
"node_modules/asynckit": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
|
@ -4481,13 +4568,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.13.4",
|
"version": "1.13.5",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
|
||||||
"integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==",
|
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.6",
|
"follow-redirects": "^1.15.11",
|
||||||
"form-data": "^4.0.4",
|
"form-data": "^4.0.5",
|
||||||
"proxy-from-env": "^1.1.0"
|
"proxy-from-env": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -5272,9 +5359,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint": {
|
"node_modules/eslint": {
|
||||||
"version": "9.39.2",
|
"version": "9.39.3",
|
||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz",
|
||||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
"integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -5284,7 +5371,7 @@
|
||||||
"@eslint/config-helpers": "^0.4.2",
|
"@eslint/config-helpers": "^0.4.2",
|
||||||
"@eslint/core": "^0.17.0",
|
"@eslint/core": "^0.17.0",
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@eslint/js": "9.39.2",
|
"@eslint/js": "9.39.3",
|
||||||
"@eslint/plugin-kit": "^0.4.1",
|
"@eslint/plugin-kit": "^0.4.1",
|
||||||
"@humanfs/node": "^0.16.6",
|
"@humanfs/node": "^0.16.6",
|
||||||
"@humanwhocodes/module-importer": "^1.0.1",
|
"@humanwhocodes/module-importer": "^1.0.1",
|
||||||
|
|
@ -5860,6 +5947,13 @@
|
||||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html-escaper": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/http-proxy-agent": {
|
"node_modules/http-proxy-agent": {
|
||||||
"version": "7.0.2",
|
"version": "7.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||||
|
|
@ -6030,6 +6124,45 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/istanbul-lib-coverage": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/istanbul-lib-report": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"istanbul-lib-coverage": "^3.0.0",
|
||||||
|
"make-dir": "^4.0.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/istanbul-reports": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"html-escaper": "^2.0.0",
|
||||||
|
"istanbul-lib-report": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jiti": {
|
"node_modules/jiti": {
|
||||||
"version": "1.21.7",
|
"version": "1.21.7",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
||||||
|
|
@ -6255,6 +6388,47 @@
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/magicast": {
|
||||||
|
"version": "0.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz",
|
||||||
|
"integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/parser": "^7.29.0",
|
||||||
|
"@babel/types": "^7.29.0",
|
||||||
|
"source-map-js": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/make-dir": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"semver": "^7.5.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/make-dir/node_modules/semver": {
|
||||||
|
"version": "7.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||||
|
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
|
@ -6327,9 +6501,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
|
||||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -7698,16 +7872,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript-eslint": {
|
"node_modules/typescript-eslint": {
|
||||||
"version": "8.54.0",
|
"version": "8.56.1",
|
||||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz",
|
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.1.tgz",
|
||||||
"integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==",
|
"integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "8.54.0",
|
"@typescript-eslint/eslint-plugin": "8.56.1",
|
||||||
"@typescript-eslint/parser": "8.54.0",
|
"@typescript-eslint/parser": "8.56.1",
|
||||||
"@typescript-eslint/typescript-estree": "8.54.0",
|
"@typescript-eslint/typescript-estree": "8.56.1",
|
||||||
"@typescript-eslint/utils": "8.54.0"
|
"@typescript-eslint/utils": "8.56.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|
@ -7717,7 +7891,7 @@
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": "^8.57.0 || ^9.0.0",
|
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||||
"typescript": ">=4.8.4 <6.0.0"
|
"typescript": ">=4.8.4 <6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
"@radix-ui/react-toast": "^1.2.15",
|
"@radix-ui/react-toast": "^1.2.15",
|
||||||
"@sentry/react": "^10.39.0",
|
"@sentry/react": "^10.39.0",
|
||||||
"@tanstack/react-query": "^5.90.12",
|
"@tanstack/react-query": "^5.90.12",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.5",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
|
@ -51,6 +51,7 @@
|
||||||
"@types/react": "^19.2.5",
|
"@types/react": "^19.2.5",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^5.1.1",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
|
"@vitest/coverage-v8": "^4.0.18",
|
||||||
"@vitest/ui": "^4.0.18",
|
"@vitest/ui": "^4.0.18",
|
||||||
"autoprefixer": "^10.4.23",
|
"autoprefixer": "^10.4.23",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
|
|
|
||||||
7
public/admin-icon.svg
Normal file
7
public/admin-icon.svg
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||||
|
<rect width="64" height="64" rx="12" fill="#3B82F6"/>
|
||||||
|
<path d="M32 18C28.6863 18 26 20.6863 26 24C26 27.3137 28.6863 30 32 30C35.3137 30 38 27.3137 38 24C38 20.6863 35.3137 18 32 18Z" fill="white"/>
|
||||||
|
<path d="M32 32C24.268 32 18 35.582 18 40V44C18 45.1046 18.8954 46 20 46H44C45.1046 46 46 45.1046 46 44V40C46 35.582 39.732 32 32 32Z" fill="white"/>
|
||||||
|
<circle cx="44" cy="20" r="8" fill="#EF4444"/>
|
||||||
|
<path d="M44 17V23M41 20H47" stroke="white" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 572 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
|
|
@ -1,27 +0,0 @@
|
||||||
// Temporary debug component - Remove after fixing login issue
|
|
||||||
import { useEffect } from 'react'
|
|
||||||
|
|
||||||
export function DebugLogin() {
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('=== LOGIN DEBUG INFO ===')
|
|
||||||
console.log('Current path:', window.location.pathname)
|
|
||||||
console.log('Access token:', localStorage.getItem('access_token'))
|
|
||||||
console.log('User:', localStorage.getItem('user'))
|
|
||||||
console.log('Token exists:', !!localStorage.getItem('access_token'))
|
|
||||||
|
|
||||||
// Parse user if exists
|
|
||||||
const userStr = localStorage.getItem('user')
|
|
||||||
if (userStr) {
|
|
||||||
try {
|
|
||||||
const user = JSON.parse(userStr)
|
|
||||||
console.log('User role:', user.role)
|
|
||||||
console.log('Is admin:', user.role === 'ADMIN')
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to parse user:', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log('========================')
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
92
src/components/__tests__/ErrorBoundary.test.tsx
Normal file
92
src/components/__tests__/ErrorBoundary.test.tsx
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { render, screen } from '@/test/test-utils'
|
||||||
|
import { ErrorBoundary } from '../ErrorBoundary'
|
||||||
|
import { Sentry } from '@/lib/sentry'
|
||||||
|
|
||||||
|
// Mock Sentry
|
||||||
|
vi.mock('@/lib/sentry', () => ({
|
||||||
|
Sentry: {
|
||||||
|
captureException: vi.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Component that throws an error
|
||||||
|
const ThrowError = ({ shouldThrow }: { shouldThrow: boolean }) => {
|
||||||
|
if (shouldThrow) {
|
||||||
|
throw new Error('Test error')
|
||||||
|
}
|
||||||
|
return <div>No error</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ErrorBoundary', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
// Suppress console.error for cleaner test output
|
||||||
|
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render children when there is no error', () => {
|
||||||
|
render(
|
||||||
|
<ErrorBoundary>
|
||||||
|
<div>Test Content</div>
|
||||||
|
</ErrorBoundary>
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByText('Test Content')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render error UI when an error is thrown', () => {
|
||||||
|
render(
|
||||||
|
<ErrorBoundary>
|
||||||
|
<ThrowError shouldThrow={true} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByText('Something went wrong')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText(/An unexpected error occurred/i)).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('button', { name: /Go to Home/i })).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('button', { name: /Refresh Page/i })).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should log error to Sentry', () => {
|
||||||
|
render(
|
||||||
|
<ErrorBoundary>
|
||||||
|
<ThrowError shouldThrow={true} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(Sentry.captureException).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show error message in development mode', () => {
|
||||||
|
const originalEnv = import.meta.env.DEV
|
||||||
|
import.meta.env.DEV = true
|
||||||
|
|
||||||
|
render(
|
||||||
|
<ErrorBoundary>
|
||||||
|
<ThrowError shouldThrow={true} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByText('Test error')).toBeInTheDocument()
|
||||||
|
|
||||||
|
import.meta.env.DEV = originalEnv
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle refresh button click', () => {
|
||||||
|
render(
|
||||||
|
<ErrorBoundary>
|
||||||
|
<ThrowError shouldThrow={true} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
)
|
||||||
|
|
||||||
|
const refreshButton = screen.getByRole('button', { name: /Refresh Page/i })
|
||||||
|
|
||||||
|
// Verify button exists and is clickable
|
||||||
|
expect(refreshButton).toBeInTheDocument()
|
||||||
|
expect(refreshButton).toBeEnabled()
|
||||||
|
|
||||||
|
// Note: We can't easily test window.location.reload() in jsdom
|
||||||
|
// The button's onClick handler calls window.location.reload() which is tested manually
|
||||||
|
})
|
||||||
|
})
|
||||||
21
src/components/ui/badge-variants.ts
Normal file
21
src/components/ui/badge-variants.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { cva } from "class-variance-authority"
|
||||||
|
|
||||||
|
export const badgeVariants = cva(
|
||||||
|
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
||||||
|
secondary:
|
||||||
|
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
destructive:
|
||||||
|
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
||||||
|
outline: "text-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
@ -1,27 +1,8 @@
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
import { badgeVariants } from "./badge-variants"
|
||||||
const badgeVariants = cva(
|
|
||||||
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default:
|
|
||||||
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
|
||||||
secondary:
|
|
||||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
||||||
destructive:
|
|
||||||
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
|
||||||
outline: "text-foreground",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export interface BadgeProps
|
export interface BadgeProps
|
||||||
extends React.HTMLAttributes<HTMLDivElement>,
|
extends React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
|
@ -33,4 +14,4 @@ function Badge({ className, variant, ...props }: BadgeProps) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Badge, badgeVariants }
|
export { Badge }
|
||||||
|
|
|
||||||
31
src/components/ui/button-variants.ts
Normal file
31
src/components/ui/button-variants.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { cva } from "class-variance-authority"
|
||||||
|
|
||||||
|
export const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2",
|
||||||
|
sm: "h-8 rounded-md px-3 text-xs",
|
||||||
|
lg: "h-10 rounded-md px-8",
|
||||||
|
icon: "h-9 w-9",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
@ -1,38 +1,9 @@
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
import { buttonVariants } from "./button-variants"
|
||||||
const buttonVariants = cva(
|
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default:
|
|
||||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
|
||||||
destructive:
|
|
||||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
|
||||||
outline:
|
|
||||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
|
||||||
secondary:
|
|
||||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
default: "h-9 px-4 py-2",
|
|
||||||
sm: "h-8 rounded-md px-3 text-xs",
|
|
||||||
lg: "h-10 rounded-md px-8",
|
|
||||||
icon: "h-9 w-9",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
size: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export interface ButtonProps
|
export interface ButtonProps
|
||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
|
@ -54,4 +25,4 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
)
|
)
|
||||||
Button.displayName = "Button"
|
Button.displayName = "Button"
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
export { Button }
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Outlet, Link, useLocation, useNavigate } from "react-router-dom"
|
import { Outlet, Link, useLocation, useNavigate } from "react-router-dom"
|
||||||
import { useEffect, useState } from "react"
|
import { useState } from "react"
|
||||||
import {
|
import {
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
Users,
|
Users,
|
||||||
|
|
@ -53,19 +53,21 @@ const adminNavigationItems = [
|
||||||
export function AppShell() {
|
export function AppShell() {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [user, setUser] = useState<User | null>(null)
|
|
||||||
const [searchQuery, setSearchQuery] = useState("")
|
const [searchQuery, setSearchQuery] = useState("")
|
||||||
|
|
||||||
useEffect(() => {
|
// Initialize user from localStorage
|
||||||
|
const [user] = useState<User | null>(() => {
|
||||||
const userStr = localStorage.getItem('user')
|
const userStr = localStorage.getItem('user')
|
||||||
if (userStr) {
|
if (userStr) {
|
||||||
try {
|
try {
|
||||||
setUser(JSON.parse(userStr))
|
return JSON.parse(userStr)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to parse user data:', error)
|
console.error('Failed to parse user data:', error)
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [])
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
const isActive = (path: string) => {
|
const isActive = (path: string) => {
|
||||||
return location.pathname.startsWith(path)
|
return location.pathname.startsWith(path)
|
||||||
|
|
@ -98,7 +100,6 @@ export function AppShell() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNotificationClick = () => {
|
const handleNotificationClick = () => {
|
||||||
console.log('Notification button clicked')
|
|
||||||
navigate('/notifications')
|
navigate('/notifications')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,50 @@
|
||||||
import { describe, it, expect } from 'vitest'
|
import { describe, it, expect } from 'vitest'
|
||||||
import { cn } from '../utils'
|
import { cn } from '../utils'
|
||||||
|
|
||||||
describe('utils', () => {
|
describe('cn utility', () => {
|
||||||
describe('cn', () => {
|
it('should merge class names', () => {
|
||||||
it('should merge class names correctly', () => {
|
const result = cn('class1', 'class2')
|
||||||
const result = cn('text-red-500', 'bg-blue-500')
|
expect(result).toBe('class1 class2')
|
||||||
expect(result).toContain('text-red-500')
|
})
|
||||||
expect(result).toContain('bg-blue-500')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle conditional classes', () => {
|
it('should handle conditional classes', () => {
|
||||||
const result = cn('base-class', false && 'hidden', true && 'visible')
|
const isConditional = true
|
||||||
expect(result).toContain('base-class')
|
const isHidden = false
|
||||||
expect(result).toContain('visible')
|
const result = cn('base', isConditional && 'conditional', isHidden && 'hidden')
|
||||||
expect(result).not.toContain('hidden')
|
expect(result).toBe('base conditional')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should merge conflicting Tailwind classes', () => {
|
it('should merge Tailwind classes correctly', () => {
|
||||||
const result = cn('p-4', 'p-8')
|
const result = cn('px-2 py-1', 'px-4')
|
||||||
// tailwind-merge should keep only p-8
|
expect(result).toBe('py-1 px-4')
|
||||||
expect(result).toBe('p-8')
|
})
|
||||||
|
|
||||||
|
it('should handle arrays of classes', () => {
|
||||||
|
const result = cn(['class1', 'class2'], 'class3')
|
||||||
|
expect(result).toBe('class1 class2 class3')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle objects with boolean values', () => {
|
||||||
|
const result = cn({
|
||||||
|
'class1': true,
|
||||||
|
'class2': false,
|
||||||
|
'class3': true,
|
||||||
})
|
})
|
||||||
|
expect(result).toBe('class1 class3')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle undefined and null values', () => {
|
||||||
|
const result = cn('class1', undefined, null, 'class2')
|
||||||
|
expect(result).toBe('class1 class2')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle empty input', () => {
|
||||||
|
const result = cn()
|
||||||
|
expect(result).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should merge conflicting Tailwind classes', () => {
|
||||||
|
const result = cn('text-red-500', 'text-blue-500')
|
||||||
|
expect(result).toBe('text-blue-500')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import {
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table"
|
} from "@/components/ui/table"
|
||||||
import { Search, Download, Eye, ChevronLeft, ChevronRight } from "lucide-react"
|
import { Search, Download, Eye, ChevronLeft, ChevronRight } from "lucide-react"
|
||||||
import { auditService } from "@/services"
|
import { auditService, type AuditLog } from "@/services"
|
||||||
import { format } from "date-fns"
|
import { format } from "date-fns"
|
||||||
|
|
||||||
export default function ActivityLogPage() {
|
export default function ActivityLogPage() {
|
||||||
|
|
@ -26,7 +26,7 @@ export default function ActivityLogPage() {
|
||||||
const { data: auditData, isLoading } = useQuery({
|
const { data: auditData, isLoading } = useQuery({
|
||||||
queryKey: ['activity-log', page, limit, search, actionFilter, resourceTypeFilter],
|
queryKey: ['activity-log', page, limit, search, actionFilter, resourceTypeFilter],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const params: any = { page, limit }
|
const params: Record<string, string | number> = { page, limit }
|
||||||
if (search) params.search = search
|
if (search) params.search = search
|
||||||
if (actionFilter) params.action = actionFilter
|
if (actionFilter) params.action = actionFilter
|
||||||
if (resourceTypeFilter) params.resourceType = resourceTypeFilter
|
if (resourceTypeFilter) params.resourceType = resourceTypeFilter
|
||||||
|
|
@ -130,7 +130,7 @@ export default function ActivityLogPage() {
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{auditData?.data?.map((log: any) => (
|
{auditData?.data?.map((log: AuditLog) => (
|
||||||
<TableRow key={log.id}>
|
<TableRow key={log.id}>
|
||||||
<TableCell className="font-medium">{log.id}</TableCell>
|
<TableCell className="font-medium">{log.id}</TableCell>
|
||||||
<TableCell>{log.userId || 'N/A'}</TableCell>
|
<TableCell>{log.userId || 'N/A'}</TableCell>
|
||||||
|
|
@ -143,7 +143,7 @@ export default function ActivityLogPage() {
|
||||||
<TableCell className="font-mono text-sm">{log.resourceId}</TableCell>
|
<TableCell className="font-mono text-sm">{log.resourceId}</TableCell>
|
||||||
<TableCell>{log.ipAddress || 'N/A'}</TableCell>
|
<TableCell>{log.ipAddress || 'N/A'}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{format(new Date(log.timestamp || log.createdAt), 'MMM dd, yyyy HH:mm:ss')}
|
{format(new Date(log.timestamp), 'MMM dd, yyyy HH:mm:ss')}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Button variant="ghost" size="icon">
|
<Button variant="ghost" size="icon">
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table"
|
} from "@/components/ui/table"
|
||||||
import { analyticsService } from "@/services"
|
import { analyticsService } from "@/services"
|
||||||
|
import type { ApiUsageData } from "@/types/analytics.types"
|
||||||
|
|
||||||
export default function AnalyticsApiPage() {
|
export default function AnalyticsApiPage() {
|
||||||
const { data: apiUsage, isLoading } = useQuery({
|
const { data: apiUsage, isLoading } = useQuery({
|
||||||
|
|
@ -84,11 +85,11 @@ export default function AnalyticsApiPage() {
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{apiUsage?.map((endpoint: any, index: number) => (
|
{apiUsage?.map((endpoint: ApiUsageData, index: number) => (
|
||||||
<TableRow key={index}>
|
<TableRow key={index}>
|
||||||
<TableCell className="font-mono text-sm">{endpoint.endpoint}</TableCell>
|
<TableCell className="font-mono text-sm">{endpoint.date}</TableCell>
|
||||||
<TableCell>{endpoint.calls}</TableCell>
|
<TableCell>{endpoint.requests}</TableCell>
|
||||||
<TableCell>{endpoint.avgDuration?.toFixed(2) || 'N/A'}</TableCell>
|
<TableCell>{endpoint.avgResponseTime?.toFixed(2) || 'N/A'}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,17 @@ import { useQuery } from "@tanstack/react-query"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from "recharts"
|
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from "recharts"
|
||||||
import { analyticsService } from "@/services"
|
import { analyticsService } from "@/services"
|
||||||
|
import type { StorageByUser, StorageAnalytics } from "@/types/analytics.types"
|
||||||
|
|
||||||
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8']
|
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8']
|
||||||
|
|
||||||
|
interface ChartDataItem {
|
||||||
|
name: string
|
||||||
|
value: number
|
||||||
|
}
|
||||||
|
|
||||||
export default function AnalyticsStoragePage() {
|
export default function AnalyticsStoragePage() {
|
||||||
const { data: storage, isLoading } = useQuery({
|
const { data: storage, isLoading } = useQuery<StorageAnalytics>({
|
||||||
queryKey: ['admin', 'analytics', 'storage'],
|
queryKey: ['admin', 'analytics', 'storage'],
|
||||||
queryFn: () => analyticsService.getStorageAnalytics(),
|
queryFn: () => analyticsService.getStorageAnalytics(),
|
||||||
})
|
})
|
||||||
|
|
@ -19,7 +25,7 @@ export default function AnalyticsStoragePage() {
|
||||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
|
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
const chartData = storage?.byCategory?.map((cat: any) => ({
|
const chartData: ChartDataItem[] = storage?.byCategory?.map((cat) => ({
|
||||||
name: cat.category,
|
name: cat.category,
|
||||||
value: cat.size,
|
value: cat.size,
|
||||||
})) || []
|
})) || []
|
||||||
|
|
@ -73,7 +79,7 @@ export default function AnalyticsStoragePage() {
|
||||||
fill="#8884d8"
|
fill="#8884d8"
|
||||||
dataKey="value"
|
dataKey="value"
|
||||||
>
|
>
|
||||||
{chartData.map((_entry: any, index: number) => (
|
{chartData.map((_entry: ChartDataItem, index: number) => (
|
||||||
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
||||||
))}
|
))}
|
||||||
</Pie>
|
</Pie>
|
||||||
|
|
@ -97,13 +103,13 @@ export default function AnalyticsStoragePage() {
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{storage.topUsers.map((user: any, index: number) => (
|
{storage.topUsers.map((user: StorageByUser, index: number) => (
|
||||||
<div key={index} className="flex items-center justify-between p-2 border rounded">
|
<div key={index} className="flex items-center justify-between p-2 border rounded">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">{user.user}</p>
|
<p className="font-medium">{user.userName || user.email}</p>
|
||||||
<p className="text-sm text-muted-foreground">{user.files} files</p>
|
<p className="text-sm text-muted-foreground">{user.documentCount} files</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="font-medium">{formatBytes(user.size)}</p>
|
<p className="font-medium">{formatBytes(user.storageUsed)}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,17 @@ import {
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
import { Plus, Edit, Trash2 } from "lucide-react"
|
import { Plus, Edit, Trash2 } from "lucide-react"
|
||||||
import { announcementService } from "@/services"
|
import { announcementService, type Announcement, type CreateAnnouncementData } from "@/services"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { format } from "date-fns"
|
import { format } from "date-fns"
|
||||||
|
import type { ApiError } from "@/types/error.types"
|
||||||
|
|
||||||
export default function AnnouncementsPage() {
|
export default function AnnouncementsPage() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
||||||
const [formDialogOpen, setFormDialogOpen] = useState(false)
|
const [formDialogOpen, setFormDialogOpen] = useState(false)
|
||||||
const [selectedAnnouncement, setSelectedAnnouncement] = useState<any>(null)
|
const [selectedAnnouncement, setSelectedAnnouncement] = useState<Announcement | null>(null)
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState<CreateAnnouncementData>({
|
||||||
title: '',
|
title: '',
|
||||||
message: '',
|
message: '',
|
||||||
type: 'info' as 'info' | 'warning' | 'success' | 'error',
|
type: 'info' as 'info' | 'warning' | 'success' | 'error',
|
||||||
|
|
@ -45,20 +46,21 @@ export default function AnnouncementsPage() {
|
||||||
})
|
})
|
||||||
|
|
||||||
const createMutation = useMutation({
|
const createMutation = useMutation({
|
||||||
mutationFn: (data: any) => announcementService.createAnnouncement(data),
|
mutationFn: (data: CreateAnnouncementData) => announcementService.createAnnouncement(data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['admin', 'announcements'] })
|
queryClient.invalidateQueries({ queryKey: ['admin', 'announcements'] })
|
||||||
toast.success("Announcement created successfully")
|
toast.success("Announcement created successfully")
|
||||||
setFormDialogOpen(false)
|
setFormDialogOpen(false)
|
||||||
resetForm()
|
resetForm()
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error) => {
|
||||||
toast.error(error.response?.data?.message || "Failed to create announcement")
|
const apiError = error as ApiError
|
||||||
|
toast.error(apiError.response?.data?.message || "Failed to create announcement")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: ({ id, data }: { id: string; data: any }) =>
|
mutationFn: ({ id, data }: { id: string; data: CreateAnnouncementData }) =>
|
||||||
announcementService.updateAnnouncement(id, data),
|
announcementService.updateAnnouncement(id, data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['admin', 'announcements'] })
|
queryClient.invalidateQueries({ queryKey: ['admin', 'announcements'] })
|
||||||
|
|
@ -66,8 +68,9 @@ export default function AnnouncementsPage() {
|
||||||
setFormDialogOpen(false)
|
setFormDialogOpen(false)
|
||||||
resetForm()
|
resetForm()
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error) => {
|
||||||
toast.error(error.response?.data?.message || "Failed to update announcement")
|
const apiError = error as ApiError
|
||||||
|
toast.error(apiError.response?.data?.message || "Failed to update announcement")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -78,8 +81,9 @@ export default function AnnouncementsPage() {
|
||||||
toast.success("Announcement deleted successfully")
|
toast.success("Announcement deleted successfully")
|
||||||
setDeleteDialogOpen(false)
|
setDeleteDialogOpen(false)
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error) => {
|
||||||
toast.error(error.response?.data?.message || "Failed to delete announcement")
|
const apiError = error as ApiError
|
||||||
|
toast.error(apiError.response?.data?.message || "Failed to delete announcement")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -101,7 +105,7 @@ export default function AnnouncementsPage() {
|
||||||
setFormDialogOpen(true)
|
setFormDialogOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOpenEditDialog = (announcement: any) => {
|
const handleOpenEditDialog = (announcement: Announcement) => {
|
||||||
setSelectedAnnouncement(announcement)
|
setSelectedAnnouncement(announcement)
|
||||||
setFormData({
|
setFormData({
|
||||||
title: announcement.title || '',
|
title: announcement.title || '',
|
||||||
|
|
@ -174,7 +178,7 @@ export default function AnnouncementsPage() {
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{announcements?.map((announcement: any) => (
|
{announcements?.map((announcement: Announcement) => (
|
||||||
<TableRow key={announcement.id}>
|
<TableRow key={announcement.id}>
|
||||||
<TableCell className="font-medium">{announcement.title}</TableCell>
|
<TableCell className="font-medium">{announcement.title}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
|
@ -268,7 +272,7 @@ export default function AnnouncementsPage() {
|
||||||
<select
|
<select
|
||||||
className="w-full px-3 py-2 border rounded-md"
|
className="w-full px-3 py-2 border rounded-md"
|
||||||
value={formData.type}
|
value={formData.type}
|
||||||
onChange={(e) => setFormData({ ...formData, type: e.target.value as any })}
|
onChange={(e) => setFormData({ ...formData, type: e.target.value as 'info' | 'warning' | 'success' | 'error' })}
|
||||||
>
|
>
|
||||||
<option value="info">Info</option>
|
<option value="info">Info</option>
|
||||||
<option value="warning">Warning</option>
|
<option value="warning">Warning</option>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import {
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table"
|
} from "@/components/ui/table"
|
||||||
import { Search, Eye } from "lucide-react"
|
import { Search, Eye } from "lucide-react"
|
||||||
import { auditService } from "@/services"
|
import { auditService, type AuditLog } from "@/services"
|
||||||
import { format } from "date-fns"
|
import { format } from "date-fns"
|
||||||
|
|
||||||
export default function AuditPage() {
|
export default function AuditPage() {
|
||||||
|
|
@ -24,7 +24,7 @@ export default function AuditPage() {
|
||||||
const { data: auditData, isLoading } = useQuery({
|
const { data: auditData, isLoading } = useQuery({
|
||||||
queryKey: ['admin', 'audit', 'logs', page, limit, search],
|
queryKey: ['admin', 'audit', 'logs', page, limit, search],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const params: any = { page, limit }
|
const params: Record<string, string | number> = { page, limit }
|
||||||
if (search) params.search = search
|
if (search) params.search = search
|
||||||
return await auditService.getAuditLogs(params)
|
return await auditService.getAuditLogs(params)
|
||||||
},
|
},
|
||||||
|
|
@ -69,7 +69,7 @@ export default function AuditPage() {
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{auditData?.data?.map((log: any) => (
|
{auditData?.data?.map((log: AuditLog) => (
|
||||||
<TableRow key={log.id}>
|
<TableRow key={log.id}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge>{log.action}</Badge>
|
<Badge>{log.action}</Badge>
|
||||||
|
|
@ -79,7 +79,7 @@ export default function AuditPage() {
|
||||||
<TableCell>{log.userId || 'N/A'}</TableCell>
|
<TableCell>{log.userId || 'N/A'}</TableCell>
|
||||||
<TableCell>{log.ipAddress || 'N/A'}</TableCell>
|
<TableCell>{log.ipAddress || 'N/A'}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{format(new Date(log.createdAt), 'MMM dd, yyyy HH:mm')}
|
{format(new Date(log.timestamp), 'MMM dd, yyyy HH:mm')}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Button variant="ghost" size="icon">
|
<Button variant="ghost" size="icon">
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { Badge } from "@/components/ui/badge"
|
||||||
import { systemService } from "@/services"
|
import { systemService } from "@/services"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
import type { ApiError } from "@/types/error.types"
|
||||||
|
|
||||||
export default function MaintenancePage() {
|
export default function MaintenancePage() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
@ -24,8 +25,9 @@ export default function MaintenancePage() {
|
||||||
toast.success("Maintenance mode enabled")
|
toast.success("Maintenance mode enabled")
|
||||||
setMessage("")
|
setMessage("")
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error) => {
|
||||||
toast.error(error.response?.data?.message || "Failed to enable maintenance mode")
|
const apiError = error as ApiError
|
||||||
|
toast.error(apiError.response?.data?.message || "Failed to enable maintenance mode")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -35,8 +37,9 @@ export default function MaintenancePage() {
|
||||||
queryClient.invalidateQueries({ queryKey: ['admin', 'maintenance'] })
|
queryClient.invalidateQueries({ queryKey: ['admin', 'maintenance'] })
|
||||||
toast.success("Maintenance mode disabled")
|
toast.success("Maintenance mode disabled")
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error) => {
|
||||||
toast.error(error.response?.data?.message || "Failed to disable maintenance mode")
|
const apiError = error as ApiError
|
||||||
|
toast.error(apiError.response?.data?.message || "Failed to disable maintenance mode")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,10 @@ import {
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table"
|
} from "@/components/ui/table"
|
||||||
import { Ban } from "lucide-react"
|
import { Ban } from "lucide-react"
|
||||||
import { securityService } from "@/services"
|
import { securityService, type ApiKey } from "@/services"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { format } from "date-fns"
|
import { format } from "date-fns"
|
||||||
|
import type { ApiError } from "@/types/error.types"
|
||||||
|
|
||||||
export default function ApiKeysPage() {
|
export default function ApiKeysPage() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
@ -29,8 +30,9 @@ export default function ApiKeysPage() {
|
||||||
queryClient.invalidateQueries({ queryKey: ['admin', 'security', 'api-keys'] })
|
queryClient.invalidateQueries({ queryKey: ['admin', 'security', 'api-keys'] })
|
||||||
toast.success("API key revoked successfully")
|
toast.success("API key revoked successfully")
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error) => {
|
||||||
toast.error(error.response?.data?.message || "Failed to revoke API key")
|
const apiError = error as ApiError
|
||||||
|
toast.error(apiError.response?.data?.message || "Failed to revoke API key")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -58,20 +60,20 @@ export default function ApiKeysPage() {
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{apiKeys?.map((key: any) => (
|
{apiKeys?.map((key: ApiKey) => (
|
||||||
<TableRow key={key.id}>
|
<TableRow key={key.id}>
|
||||||
<TableCell className="font-medium">{key.name}</TableCell>
|
<TableCell className="font-medium">{key.name}</TableCell>
|
||||||
<TableCell>{key.userId || 'N/A'}</TableCell>
|
<TableCell>{key.userId || 'N/A'}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{key.lastUsedAt ? format(new Date(key.lastUsedAt), 'MMM dd, yyyy') : 'Never'}
|
{key.lastUsed ? format(new Date(key.lastUsed), 'MMM dd, yyyy') : 'Never'}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant={key.revoked ? 'destructive' : 'default'}>
|
<Badge variant={key.isActive ? 'default' : 'destructive'}>
|
||||||
{key.revoked ? 'Revoked' : 'Active'}
|
{key.isActive ? 'Active' : 'Revoked'}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{!key.revoked && (
|
{key.isActive && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import {
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table"
|
} from "@/components/ui/table"
|
||||||
import { Search, Ban } from "lucide-react"
|
import { Search, Ban } from "lucide-react"
|
||||||
import { securityService } from "@/services"
|
import { securityService, type FailedLogin } from "@/services"
|
||||||
import { format } from "date-fns"
|
import { format } from "date-fns"
|
||||||
|
|
||||||
export default function FailedLoginsPage() {
|
export default function FailedLoginsPage() {
|
||||||
|
|
@ -24,7 +24,7 @@ export default function FailedLoginsPage() {
|
||||||
const { data: failedLogins, isLoading } = useQuery({
|
const { data: failedLogins, isLoading } = useQuery({
|
||||||
queryKey: ['admin', 'security', 'failed-logins', page, limit, search],
|
queryKey: ['admin', 'security', 'failed-logins', page, limit, search],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const params: any = { page, limit }
|
const params: Record<string, string | number> = { page, limit }
|
||||||
if (search) params.email = search
|
if (search) params.email = search
|
||||||
return await securityService.getFailedLogins(params)
|
return await securityService.getFailedLogins(params)
|
||||||
},
|
},
|
||||||
|
|
@ -67,18 +67,18 @@ export default function FailedLoginsPage() {
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{failedLogins?.data?.map((login: any) => (
|
{failedLogins?.data?.map((login: FailedLogin) => (
|
||||||
<TableRow key={login.id}>
|
<TableRow key={login.id}>
|
||||||
<TableCell className="font-medium">{login.email}</TableCell>
|
<TableCell className="font-medium">{login.email}</TableCell>
|
||||||
<TableCell className="font-mono text-sm">{login.ipAddress}</TableCell>
|
<TableCell className="font-mono text-sm">{login.ipAddress}</TableCell>
|
||||||
<TableCell className="max-w-xs truncate">{login.userAgent}</TableCell>
|
<TableCell className="max-w-xs truncate">{login.ipAddress}</TableCell>
|
||||||
<TableCell>{login.reason || 'N/A'}</TableCell>
|
<TableCell>{login.reason || 'N/A'}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{format(new Date(login.attemptedAt), 'MMM dd, yyyy HH:mm')}
|
{format(new Date(login.timestamp), 'MMM dd, yyyy HH:mm')}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant={login.blocked ? 'destructive' : 'secondary'}>
|
<Badge variant="secondary">
|
||||||
{login.blocked ? 'Yes' : 'No'}
|
N/A
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table"
|
} from "@/components/ui/table"
|
||||||
import { securityService } from "@/services"
|
import { securityService } from "@/services"
|
||||||
|
import type { RateLimitViolation } from "@/types/security.types"
|
||||||
|
|
||||||
export default function RateLimitsPage() {
|
export default function RateLimitsPage() {
|
||||||
const { data: violations, isLoading } = useQuery({
|
const { data: violations, isLoading } = useQuery({
|
||||||
|
|
@ -39,7 +40,7 @@ export default function RateLimitsPage() {
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{violations?.map((violation: any) => (
|
{violations?.map((violation: RateLimitViolation) => (
|
||||||
<TableRow key={violation.id}>
|
<TableRow key={violation.id}>
|
||||||
<TableCell>{violation.userId || 'N/A'}</TableCell>
|
<TableCell>{violation.userId || 'N/A'}</TableCell>
|
||||||
<TableCell className="font-mono text-sm">{violation.ipAddress}</TableCell>
|
<TableCell className="font-mono text-sm">{violation.ipAddress}</TableCell>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import {
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table"
|
} from "@/components/ui/table"
|
||||||
import { LogOut } from "lucide-react"
|
import { LogOut } from "lucide-react"
|
||||||
import { securityService } from "@/services"
|
import { securityService, type ActiveSession } from "@/services"
|
||||||
import { format } from "date-fns"
|
import { format } from "date-fns"
|
||||||
|
|
||||||
export default function SessionsPage() {
|
export default function SessionsPage() {
|
||||||
|
|
@ -43,7 +43,7 @@ export default function SessionsPage() {
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{sessions?.map((session: any) => (
|
{sessions?.map((session: ActiveSession) => (
|
||||||
<TableRow key={session.id}>
|
<TableRow key={session.id}>
|
||||||
<TableCell className="font-medium">{session.userId || 'N/A'}</TableCell>
|
<TableCell className="font-medium">{session.userId || 'N/A'}</TableCell>
|
||||||
<TableCell className="font-mono text-sm">{session.ipAddress}</TableCell>
|
<TableCell className="font-mono text-sm">{session.ipAddress}</TableCell>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Shield, Ban } from "lucide-react"
|
import { Shield, Ban } from "lucide-react"
|
||||||
import { securityService } from "@/services"
|
import { securityService } from "@/services"
|
||||||
|
import type { SuspiciousIP, SuspiciousEmail } from "@/types/security.types"
|
||||||
|
|
||||||
export default function SuspiciousActivityPage() {
|
export default function SuspiciousActivityPage() {
|
||||||
const { data: suspicious, isLoading } = useQuery({
|
const { data: suspicious, isLoading } = useQuery({
|
||||||
|
|
@ -27,7 +28,7 @@ export default function SuspiciousActivityPage() {
|
||||||
<div className="text-center py-8">Loading...</div>
|
<div className="text-center py-8">Loading...</div>
|
||||||
) : (suspicious?.suspiciousIPs?.length ?? 0) > 0 ? (
|
) : (suspicious?.suspiciousIPs?.length ?? 0) > 0 ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{suspicious?.suspiciousIPs?.map((ip: any, index: number) => (
|
{suspicious?.suspiciousIPs?.map((ip: SuspiciousIP, index: number) => (
|
||||||
<div key={index} className="flex items-center justify-between p-2 border rounded">
|
<div key={index} className="flex items-center justify-between p-2 border rounded">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-mono font-medium">{ip.ipAddress}</p>
|
<p className="font-mono font-medium">{ip.ipAddress}</p>
|
||||||
|
|
@ -60,7 +61,7 @@ export default function SuspiciousActivityPage() {
|
||||||
<div className="text-center py-8">Loading...</div>
|
<div className="text-center py-8">Loading...</div>
|
||||||
) : (suspicious?.suspiciousEmails?.length ?? 0) > 0 ? (
|
) : (suspicious?.suspiciousEmails?.length ?? 0) > 0 ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{suspicious?.suspiciousEmails?.map((email: any, index: number) => (
|
{suspicious?.suspiciousEmails?.map((email: SuspiciousEmail, index: number) => (
|
||||||
<div key={index} className="flex items-center justify-between p-2 border rounded">
|
<div key={index} className="flex items-center justify-between p-2 border rounded">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">{email.email}</p>
|
<p className="font-medium">{email.email}</p>
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,9 @@ import {
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
import { Plus } from "lucide-react"
|
import { Plus } from "lucide-react"
|
||||||
import { settingsService } from "@/services"
|
import { settingsService, type Setting } from "@/services"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
import type { ApiError } from "@/types/error.types"
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
@ -41,8 +42,9 @@ export default function SettingsPage() {
|
||||||
queryClient.invalidateQueries({ queryKey: ['admin', 'settings'] })
|
queryClient.invalidateQueries({ queryKey: ['admin', 'settings'] })
|
||||||
toast.success("Setting updated successfully")
|
toast.success("Setting updated successfully")
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error) => {
|
||||||
toast.error(error.response?.data?.message || "Failed to update setting")
|
const apiError = error as ApiError
|
||||||
|
toast.error(apiError.response?.data?.message || "Failed to update setting")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -60,8 +62,9 @@ export default function SettingsPage() {
|
||||||
setCreateDialogOpen(false)
|
setCreateDialogOpen(false)
|
||||||
setNewSetting({ key: "", value: "", description: "", isPublic: false })
|
setNewSetting({ key: "", value: "", description: "", isPublic: false })
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error) => {
|
||||||
toast.error(error.response?.data?.message || "Failed to create setting")
|
const apiError = error as ApiError
|
||||||
|
toast.error(apiError.response?.data?.message || "Failed to create setting")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -112,8 +115,8 @@ export default function SettingsPage() {
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="text-center py-8">Loading settings...</div>
|
<div className="text-center py-8">Loading settings...</div>
|
||||||
) : settings && settings.length > 0 ? (
|
) : settings && settings.length > 0 ? (
|
||||||
settings.map((setting: any) => (
|
settings.map((setting: Setting) => (
|
||||||
<div key={setting.id} className="space-y-2">
|
<div key={setting.key} className="space-y-2">
|
||||||
<Label htmlFor={setting.key}>{setting.key}</Label>
|
<Label htmlFor={setting.key}>{setting.key}</Label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,14 @@ import { ArrowLeft } from "lucide-react"
|
||||||
import { userService } from "@/services"
|
import { userService } from "@/services"
|
||||||
import { format } from "date-fns"
|
import { format } from "date-fns"
|
||||||
|
|
||||||
|
interface ActivityItem {
|
||||||
|
action?: string
|
||||||
|
description?: string
|
||||||
|
message?: string
|
||||||
|
createdAt?: string
|
||||||
|
timestamp?: string
|
||||||
|
}
|
||||||
|
|
||||||
export default function UserActivityPage() {
|
export default function UserActivityPage() {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
@ -36,7 +44,7 @@ export default function UserActivityPage() {
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{activity && activity.length > 0 ? (
|
{activity && activity.length > 0 ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{activity.map((item: any, index: number) => (
|
{activity.map((item: ActivityItem, index: number) => (
|
||||||
<div key={index} className="border-l-2 pl-4 pb-4">
|
<div key={index} className="border-l-2 pl-4 pb-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -44,7 +52,7 @@ export default function UserActivityPage() {
|
||||||
<p className="text-sm text-muted-foreground">{item.description || item.message}</p>
|
<p className="text-sm text-muted-foreground">{item.description || item.message}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
{format(new Date(item.createdAt || item.timestamp), 'PPpp')}
|
{(item.createdAt || item.timestamp) && format(new Date(item.createdAt || item.timestamp!), 'PPpp')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,17 @@ import { Search, Download, Eye, UserPlus, Trash2, Key, Upload } from "lucide-rea
|
||||||
import { userService } from "@/services"
|
import { userService } from "@/services"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { format } from "date-fns"
|
import { format } from "date-fns"
|
||||||
|
import type { ApiError } from "@/types/error.types"
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: string
|
||||||
|
email: string
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
role: string
|
||||||
|
isActive: boolean
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
export default function UsersPage() {
|
export default function UsersPage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
@ -42,7 +53,7 @@ export default function UsersPage() {
|
||||||
const [search, setSearch] = useState("")
|
const [search, setSearch] = useState("")
|
||||||
const [roleFilter, setRoleFilter] = useState<string>("all")
|
const [roleFilter, setRoleFilter] = useState<string>("all")
|
||||||
const [statusFilter, setStatusFilter] = useState<string>("all")
|
const [statusFilter, setStatusFilter] = useState<string>("all")
|
||||||
const [selectedUser, setSelectedUser] = useState<any>(null)
|
const [selectedUser, setSelectedUser] = useState<User | null>(null)
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
||||||
const [resetPasswordDialogOpen, setResetPasswordDialogOpen] = useState(false)
|
const [resetPasswordDialogOpen, setResetPasswordDialogOpen] = useState(false)
|
||||||
const [importDialogOpen, setImportDialogOpen] = useState(false)
|
const [importDialogOpen, setImportDialogOpen] = useState(false)
|
||||||
|
|
@ -51,7 +62,7 @@ export default function UsersPage() {
|
||||||
const { data: usersData, isLoading } = useQuery({
|
const { data: usersData, isLoading } = useQuery({
|
||||||
queryKey: ['admin', 'users', page, limit, search, roleFilter, statusFilter],
|
queryKey: ['admin', 'users', page, limit, search, roleFilter, statusFilter],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const params: any = { page, limit }
|
const params: Record<string, string | number | boolean> = { page, limit }
|
||||||
if (search) params.search = search
|
if (search) params.search = search
|
||||||
if (roleFilter !== 'all') params.role = roleFilter
|
if (roleFilter !== 'all') params.role = roleFilter
|
||||||
if (statusFilter !== 'all') params.isActive = statusFilter === 'active'
|
if (statusFilter !== 'all') params.isActive = statusFilter === 'active'
|
||||||
|
|
@ -67,8 +78,9 @@ export default function UsersPage() {
|
||||||
toast.success("User deleted successfully")
|
toast.success("User deleted successfully")
|
||||||
setDeleteDialogOpen(false)
|
setDeleteDialogOpen(false)
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error) => {
|
||||||
toast.error(error.response?.data?.message || "Failed to delete user")
|
const apiError = error as ApiError
|
||||||
|
toast.error(apiError.response?.data?.message || "Failed to delete user")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -78,8 +90,9 @@ export default function UsersPage() {
|
||||||
toast.success(`Password reset. Temporary password: ${data.temporaryPassword}`)
|
toast.success(`Password reset. Temporary password: ${data.temporaryPassword}`)
|
||||||
setResetPasswordDialogOpen(false)
|
setResetPasswordDialogOpen(false)
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error) => {
|
||||||
toast.error(error.response?.data?.message || "Failed to reset password")
|
const apiError = error as ApiError
|
||||||
|
toast.error(apiError.response?.data?.message || "Failed to reset password")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -91,8 +104,9 @@ export default function UsersPage() {
|
||||||
setImportDialogOpen(false)
|
setImportDialogOpen(false)
|
||||||
setImportFile(null)
|
setImportFile(null)
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error) => {
|
||||||
toast.error(error.response?.data?.message || "Failed to import users")
|
const apiError = error as ApiError
|
||||||
|
toast.error(apiError.response?.data?.message || "Failed to import users")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -105,8 +119,9 @@ export default function UsersPage() {
|
||||||
a.download = `users-${new Date().toISOString()}.csv`
|
a.download = `users-${new Date().toISOString()}.csv`
|
||||||
a.click()
|
a.click()
|
||||||
toast.success("Users exported successfully")
|
toast.success("Users exported successfully")
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
toast.error(error.response?.data?.message || "Failed to export users")
|
const apiError = error as ApiError
|
||||||
|
toast.error(apiError.response?.data?.message || "Failed to export users")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -223,7 +238,7 @@ export default function UsersPage() {
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{usersData?.data?.map((user: any) => (
|
{usersData?.data?.map((user: User) => (
|
||||||
<TableRow key={user.id}>
|
<TableRow key={user.id}>
|
||||||
<TableCell className="font-medium">{user.email}</TableCell>
|
<TableCell className="font-medium">{user.email}</TableCell>
|
||||||
<TableCell>{user.firstName} {user.lastName}</TableCell>
|
<TableCell>{user.firstName} {user.lastName}</TableCell>
|
||||||
|
|
|
||||||
|
|
@ -11,16 +11,6 @@ vi.mock('@/services', () => ({
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock react-router-dom
|
|
||||||
vi.mock('react-router-dom', async () => {
|
|
||||||
const actual = await vi.importActual('react-router-dom')
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
useNavigate: () => vi.fn(),
|
|
||||||
useLocation: () => ({ state: null }),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('LoginPage', () => {
|
describe('LoginPage', () => {
|
||||||
it('should render login form', () => {
|
it('should render login form', () => {
|
||||||
render(<LoginPage />)
|
render(<LoginPage />)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { Eye, EyeOff } from "lucide-react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { authService } from "@/services"
|
import { authService } from "@/services"
|
||||||
import { errorTracker } from "@/lib/error-tracker"
|
import { errorTracker } from "@/lib/error-tracker"
|
||||||
|
import type { ApiError, LocationState } from "@/types/error.types"
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
@ -17,7 +18,7 @@ export default function LoginPage() {
|
||||||
const [showPassword, setShowPassword] = useState(false)
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
const from = (location.state as any)?.from?.pathname || "/admin/dashboard"
|
const from = (location.state as LocationState)?.from?.pathname || "/admin/dashboard"
|
||||||
|
|
||||||
const handleLogin = async (e: React.FormEvent) => {
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
@ -45,13 +46,14 @@ export default function LoginPage() {
|
||||||
|
|
||||||
// Navigate to dashboard
|
// Navigate to dashboard
|
||||||
navigate(from, { replace: true })
|
navigate(from, { replace: true })
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
console.error('Login error:', error)
|
const apiError = error as ApiError
|
||||||
const message = error.response?.data?.message || "Invalid email or password"
|
console.error('Login error:', apiError)
|
||||||
|
const message = apiError.response?.data?.message || "Invalid email or password"
|
||||||
toast.error(message)
|
toast.error(message)
|
||||||
|
|
||||||
// Track login error
|
// Track login error
|
||||||
errorTracker.trackError(error, {
|
errorTracker.trackError(apiError, {
|
||||||
extra: { email, action: 'login' }
|
extra: { email, action: 'login' }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
200
src/services/__tests__/auth.service.test.ts
Normal file
200
src/services/__tests__/auth.service.test.ts
Normal file
|
|
@ -0,0 +1,200 @@
|
||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||||
|
import { authService } from '../auth.service'
|
||||||
|
import apiClient from '../api/client'
|
||||||
|
|
||||||
|
// Mock the API client
|
||||||
|
vi.mock('../api/client', () => ({
|
||||||
|
default: {
|
||||||
|
post: vi.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('AuthService', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.clear()
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
localStorage.clear()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('login', () => {
|
||||||
|
it('should login successfully and store tokens', async () => {
|
||||||
|
const mockResponse = {
|
||||||
|
data: {
|
||||||
|
accessToken: 'test-access-token',
|
||||||
|
refreshToken: 'test-refresh-token',
|
||||||
|
user: {
|
||||||
|
id: '1',
|
||||||
|
email: 'admin@example.com',
|
||||||
|
firstName: 'Admin',
|
||||||
|
lastName: 'User',
|
||||||
|
role: 'ADMIN',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mocked(apiClient.post).mockResolvedValue(mockResponse)
|
||||||
|
|
||||||
|
const result = await authService.login({
|
||||||
|
email: 'admin@example.com',
|
||||||
|
password: 'password123',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(apiClient.post).toHaveBeenCalledWith('/auth/login', {
|
||||||
|
email: 'admin@example.com',
|
||||||
|
password: 'password123',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual(mockResponse.data)
|
||||||
|
expect(localStorage.getItem('access_token')).toBe('test-access-token')
|
||||||
|
expect(localStorage.getItem('refresh_token')).toBe('test-refresh-token')
|
||||||
|
expect(localStorage.getItem('user')).toBe(JSON.stringify(mockResponse.data.user))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle login failure', async () => {
|
||||||
|
vi.mocked(apiClient.post).mockRejectedValue(new Error('Invalid credentials'))
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
authService.login({
|
||||||
|
email: 'wrong@example.com',
|
||||||
|
password: 'wrongpassword',
|
||||||
|
})
|
||||||
|
).rejects.toThrow('Invalid credentials')
|
||||||
|
|
||||||
|
expect(localStorage.getItem('access_token')).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('logout', () => {
|
||||||
|
it('should logout and clear tokens', async () => {
|
||||||
|
localStorage.setItem('access_token', 'test-token')
|
||||||
|
localStorage.setItem('refresh_token', 'test-refresh')
|
||||||
|
localStorage.setItem('user', JSON.stringify({ id: '1' }))
|
||||||
|
|
||||||
|
vi.mocked(apiClient.post).mockResolvedValue({ data: {} })
|
||||||
|
|
||||||
|
await authService.logout()
|
||||||
|
|
||||||
|
expect(apiClient.post).toHaveBeenCalledWith('/auth/logout')
|
||||||
|
expect(localStorage.getItem('access_token')).toBeNull()
|
||||||
|
expect(localStorage.getItem('refresh_token')).toBeNull()
|
||||||
|
expect(localStorage.getItem('user')).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should clear tokens even if API call fails', async () => {
|
||||||
|
localStorage.setItem('access_token', 'test-token')
|
||||||
|
vi.mocked(apiClient.post).mockRejectedValue(new Error('Network error'))
|
||||||
|
|
||||||
|
// The logout method uses try/finally, so it will throw but still clear tokens
|
||||||
|
try {
|
||||||
|
await authService.logout()
|
||||||
|
} catch {
|
||||||
|
// Error is expected
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tokens should still be cleared due to finally block
|
||||||
|
expect(localStorage.getItem('access_token')).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('refreshToken', () => {
|
||||||
|
it('should refresh access token', async () => {
|
||||||
|
localStorage.setItem('refresh_token', 'old-refresh-token')
|
||||||
|
|
||||||
|
const mockResponse = {
|
||||||
|
data: {
|
||||||
|
accessToken: 'new-access-token',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mocked(apiClient.post).mockResolvedValue(mockResponse)
|
||||||
|
|
||||||
|
const result = await authService.refreshToken()
|
||||||
|
|
||||||
|
expect(apiClient.post).toHaveBeenCalledWith('/auth/refresh', {
|
||||||
|
refreshToken: 'old-refresh-token',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual(mockResponse.data)
|
||||||
|
expect(localStorage.getItem('access_token')).toBe('new-access-token')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getCurrentUser', () => {
|
||||||
|
it('should return current user from localStorage', () => {
|
||||||
|
const user = {
|
||||||
|
id: '1',
|
||||||
|
email: 'test@example.com',
|
||||||
|
firstName: 'Test',
|
||||||
|
lastName: 'User',
|
||||||
|
role: 'ADMIN',
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem('user', JSON.stringify(user))
|
||||||
|
|
||||||
|
const result = authService.getCurrentUser()
|
||||||
|
|
||||||
|
expect(result).toEqual(user)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return null if no user in localStorage', () => {
|
||||||
|
const result = authService.getCurrentUser()
|
||||||
|
|
||||||
|
expect(result).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle invalid JSON in localStorage', () => {
|
||||||
|
localStorage.setItem('user', 'invalid-json')
|
||||||
|
|
||||||
|
expect(() => authService.getCurrentUser()).toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('isAuthenticated', () => {
|
||||||
|
it('should return true if access token exists', () => {
|
||||||
|
localStorage.setItem('access_token', 'test-token')
|
||||||
|
|
||||||
|
expect(authService.isAuthenticated()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false if no access token', () => {
|
||||||
|
expect(authService.isAuthenticated()).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('isAdmin', () => {
|
||||||
|
it('should return true if user is admin', () => {
|
||||||
|
const user = {
|
||||||
|
id: '1',
|
||||||
|
email: 'admin@example.com',
|
||||||
|
firstName: 'Admin',
|
||||||
|
lastName: 'User',
|
||||||
|
role: 'ADMIN',
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem('user', JSON.stringify(user))
|
||||||
|
|
||||||
|
expect(authService.isAdmin()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false if user is not admin', () => {
|
||||||
|
const user = {
|
||||||
|
id: '1',
|
||||||
|
email: 'user@example.com',
|
||||||
|
firstName: 'Regular',
|
||||||
|
lastName: 'User',
|
||||||
|
role: 'USER',
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem('user', JSON.stringify(user))
|
||||||
|
|
||||||
|
expect(authService.isAdmin()).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false if no user in localStorage', () => {
|
||||||
|
expect(authService.isAdmin()).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import apiClient from './api/client'
|
import apiClient from './api/client'
|
||||||
|
import type { ApiUsageData, ErrorRateSummary, StorageByUser, StorageAnalytics } from '@/types/analytics.types'
|
||||||
|
|
||||||
export interface OverviewStats {
|
export interface OverviewStats {
|
||||||
users?: {
|
users?: {
|
||||||
|
|
@ -68,8 +69,8 @@ class AnalyticsService {
|
||||||
/**
|
/**
|
||||||
* Get API usage statistics
|
* Get API usage statistics
|
||||||
*/
|
*/
|
||||||
async getApiUsage(days: number = 7): Promise<any> {
|
async getApiUsage(days: number = 7): Promise<ApiUsageData[]> {
|
||||||
const response = await apiClient.get('/admin/analytics/api-usage', {
|
const response = await apiClient.get<ApiUsageData[]>('/admin/analytics/api-usage', {
|
||||||
params: { days },
|
params: { days },
|
||||||
})
|
})
|
||||||
return response.data
|
return response.data
|
||||||
|
|
@ -78,8 +79,8 @@ class AnalyticsService {
|
||||||
/**
|
/**
|
||||||
* Get error rate statistics
|
* Get error rate statistics
|
||||||
*/
|
*/
|
||||||
async getErrorRate(days: number = 7): Promise<any> {
|
async getErrorRate(days: number = 7): Promise<ErrorRateSummary> {
|
||||||
const response = await apiClient.get('/admin/analytics/error-rate', {
|
const response = await apiClient.get<ErrorRateSummary>('/admin/analytics/error-rate', {
|
||||||
params: { days },
|
params: { days },
|
||||||
})
|
})
|
||||||
return response.data
|
return response.data
|
||||||
|
|
@ -88,8 +89,8 @@ class AnalyticsService {
|
||||||
/**
|
/**
|
||||||
* Get storage usage by user
|
* Get storage usage by user
|
||||||
*/
|
*/
|
||||||
async getStorageByUser(limit: number = 10): Promise<any> {
|
async getStorageByUser(limit: number = 10): Promise<StorageByUser[]> {
|
||||||
const response = await apiClient.get('/admin/analytics/storage/by-user', {
|
const response = await apiClient.get<StorageByUser[]>('/admin/analytics/storage/by-user', {
|
||||||
params: { limit },
|
params: { limit },
|
||||||
})
|
})
|
||||||
return response.data
|
return response.data
|
||||||
|
|
@ -98,8 +99,8 @@ class AnalyticsService {
|
||||||
/**
|
/**
|
||||||
* Get storage analytics
|
* Get storage analytics
|
||||||
*/
|
*/
|
||||||
async getStorageAnalytics(): Promise<any> {
|
async getStorageAnalytics(): Promise<StorageAnalytics> {
|
||||||
const response = await apiClient.get('/admin/analytics/storage')
|
const response = await apiClient.get<StorageAnalytics>('/admin/analytics/storage')
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
23
src/services/api/__tests__/client.test.ts
Normal file
23
src/services/api/__tests__/client.test.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { describe, it, expect, beforeEach } from 'vitest'
|
||||||
|
import apiClient from '../client'
|
||||||
|
|
||||||
|
describe('API Client', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.clear()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create axios instance with correct config', () => {
|
||||||
|
expect(apiClient.defaults.baseURL).toBeDefined()
|
||||||
|
expect(apiClient.defaults.headers['Content-Type']).toBe('application/json')
|
||||||
|
expect(apiClient.defaults.withCredentials).toBe(true)
|
||||||
|
expect(apiClient.defaults.timeout).toBe(30000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have request interceptor configured', () => {
|
||||||
|
expect(apiClient.interceptors.request.handlers?.length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have response interceptor configured', () => {
|
||||||
|
expect(apiClient.interceptors.response.handlers?.length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
import axios, { type AxiosInstance, type AxiosError } from 'axios'
|
import axios, { type AxiosInstance, type AxiosError, type InternalAxiosRequestConfig } from 'axios'
|
||||||
|
|
||||||
const API_BASE_URL = import.meta.env.VITE_BACKEND_API_URL || 'http://localhost:3001/api/v1'
|
const API_BASE_URL = import.meta.env.VITE_BACKEND_API_URL || 'http://localhost:3001/api/v1'
|
||||||
|
|
||||||
|
interface RetryableAxiosRequestConfig extends InternalAxiosRequestConfig {
|
||||||
|
_retry?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
// Create axios instance with default config
|
// Create axios instance with default config
|
||||||
const apiClient: AxiosInstance = axios.create({
|
const apiClient: AxiosInstance = axios.create({
|
||||||
baseURL: API_BASE_URL,
|
baseURL: API_BASE_URL,
|
||||||
|
|
@ -43,7 +47,7 @@ apiClient.interceptors.request.use(
|
||||||
apiClient.interceptors.response.use(
|
apiClient.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => response,
|
||||||
async (error: AxiosError) => {
|
async (error: AxiosError) => {
|
||||||
const originalRequest = error.config as any
|
const originalRequest = error.config as RetryableAxiosRequestConfig
|
||||||
|
|
||||||
// Handle 401 Unauthorized - Try to refresh token
|
// Handle 401 Unauthorized - Try to refresh token
|
||||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ export interface AuditLog {
|
||||||
action: string
|
action: string
|
||||||
resourceType: string
|
resourceType: string
|
||||||
resourceId: string
|
resourceId: string
|
||||||
changes?: Record<string, any>
|
changes?: Record<string, unknown>
|
||||||
ipAddress: string
|
ipAddress: string
|
||||||
userAgent: string
|
userAgent: string
|
||||||
timestamp: string
|
timestamp: string
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import apiClient from './api/client'
|
import apiClient from './api/client'
|
||||||
|
import type { ActivityLog } from '@/types/activity.types'
|
||||||
|
|
||||||
export interface UserDashboardStats {
|
export interface UserDashboardStats {
|
||||||
totalInvoices: number
|
totalInvoices: number
|
||||||
|
|
@ -6,13 +7,7 @@ export interface UserDashboardStats {
|
||||||
totalRevenue: number
|
totalRevenue: number
|
||||||
pendingInvoices: number
|
pendingInvoices: number
|
||||||
growthPercentage?: number
|
growthPercentage?: number
|
||||||
recentActivity?: Array<{
|
recentActivity?: ActivityLog[]
|
||||||
id: string
|
|
||||||
type: string
|
|
||||||
description: string
|
|
||||||
date: string
|
|
||||||
amount?: number
|
|
||||||
}>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserProfile {
|
export interface UserProfile {
|
||||||
|
|
@ -43,8 +38,8 @@ class DashboardService {
|
||||||
/**
|
/**
|
||||||
* Get user recent activity
|
* Get user recent activity
|
||||||
*/
|
*/
|
||||||
async getRecentActivity(limit: number = 10): Promise<any[]> {
|
async getRecentActivity(limit: number = 10): Promise<ActivityLog[]> {
|
||||||
const response = await apiClient.get('/user/activity', {
|
const response = await apiClient.get<ActivityLog[]>('/user/activity', {
|
||||||
params: { limit },
|
params: { limit },
|
||||||
})
|
})
|
||||||
return response.data
|
return response.data
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,4 @@ export type { Announcement, CreateAnnouncementData, UpdateAnnouncementData } fro
|
||||||
export type { AuditLog, GetAuditLogsParams, AuditStats } from './audit.service'
|
export type { AuditLog, GetAuditLogsParams, AuditStats } from './audit.service'
|
||||||
export type { Setting, CreateSettingData, UpdateSettingData } from './settings.service'
|
export type { Setting, CreateSettingData, UpdateSettingData } from './settings.service'
|
||||||
export type { UserDashboardStats, UserProfile } from './dashboard.service'
|
export type { UserDashboardStats, UserProfile } from './dashboard.service'
|
||||||
export type { Notification } from './notification.service'
|
export type { Notification, NotificationSettings } from './notification.service'
|
||||||
export type { NotificationSettings } from './notification.service'
|
|
||||||
export type { NotificationSettings } from './notification.service'
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import apiClient from './api/client'
|
import apiClient from './api/client'
|
||||||
|
import type { SuspiciousIP, SuspiciousEmail, RateLimitViolation } from '@/types/security.types'
|
||||||
|
|
||||||
export interface SuspiciousActivity {
|
export interface SuspiciousActivity {
|
||||||
id: string
|
id: string
|
||||||
|
|
@ -42,10 +43,13 @@ class SecurityService {
|
||||||
* Get suspicious activity logs
|
* Get suspicious activity logs
|
||||||
*/
|
*/
|
||||||
async getSuspiciousActivity(): Promise<{
|
async getSuspiciousActivity(): Promise<{
|
||||||
suspiciousIPs?: any[]
|
suspiciousIPs?: SuspiciousIP[]
|
||||||
suspiciousEmails?: any[]
|
suspiciousEmails?: SuspiciousEmail[]
|
||||||
}> {
|
}> {
|
||||||
const response = await apiClient.get('/admin/security/suspicious')
|
const response = await apiClient.get<{
|
||||||
|
suspiciousIPs?: SuspiciousIP[]
|
||||||
|
suspiciousEmails?: SuspiciousEmail[]
|
||||||
|
}>('/admin/security/suspicious')
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,15 +76,15 @@ class SecurityService {
|
||||||
limit?: number
|
limit?: number
|
||||||
email?: string
|
email?: string
|
||||||
}): Promise<{ data: FailedLogin[]; total: number }> {
|
}): Promise<{ data: FailedLogin[]; total: number }> {
|
||||||
const response = await apiClient.get('/admin/security/failed-logins', { params })
|
const response = await apiClient.get<{ data: FailedLogin[]; total: number }>('/admin/security/failed-logins', { params })
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get rate limit violations
|
* Get rate limit violations
|
||||||
*/
|
*/
|
||||||
async getRateLimitViolations(days: number = 7): Promise<any[]> {
|
async getRateLimitViolations(days: number = 7): Promise<RateLimitViolation[]> {
|
||||||
const response = await apiClient.get('/admin/security/rate-limits', {
|
const response = await apiClient.get<RateLimitViolation[]>('/admin/security/rate-limits', {
|
||||||
params: { days },
|
params: { days },
|
||||||
})
|
})
|
||||||
return response.data
|
return response.data
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import apiClient from './api/client'
|
import apiClient from './api/client'
|
||||||
|
import type { UserActivity } from '@/types/activity.types'
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string
|
id: string
|
||||||
|
|
@ -98,8 +99,8 @@ class UserService {
|
||||||
/**
|
/**
|
||||||
* Get user activity logs
|
* Get user activity logs
|
||||||
*/
|
*/
|
||||||
async getUserActivity(id: string, days: number = 30): Promise<any[]> {
|
async getUserActivity(id: string, days: number = 30): Promise<UserActivity[]> {
|
||||||
const response = await apiClient.get(`/admin/users/${id}/activity`, {
|
const response = await apiClient.get<UserActivity[]>(`/admin/users/${id}/activity`, {
|
||||||
params: { days },
|
params: { days },
|
||||||
})
|
})
|
||||||
return response.data
|
return response.data
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,30 @@
|
||||||
import { expect, afterEach } from 'vitest'
|
import { expect, afterEach, vi } from 'vitest'
|
||||||
import { cleanup } from '@testing-library/react'
|
import { cleanup } from '@testing-library/react'
|
||||||
import * as matchers from '@testing-library/jest-dom/matchers'
|
import * as matchers from '@testing-library/jest-dom/matchers'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
// Mock react-router-dom before any imports
|
||||||
|
vi.mock('react-router-dom', () => ({
|
||||||
|
BrowserRouter: ({ children }: { children: React.ReactNode }) => children,
|
||||||
|
MemoryRouter: ({ children }: { children: React.ReactNode }) => children,
|
||||||
|
Routes: ({ children }: { children: React.ReactNode }) => children,
|
||||||
|
Route: () => null,
|
||||||
|
Link: ({ children, to }: { children: React.ReactNode; to: string }) =>
|
||||||
|
React.createElement('a', { href: to }, children),
|
||||||
|
NavLink: ({ children, to }: { children: React.ReactNode; to: string }) =>
|
||||||
|
React.createElement('a', { href: to }, children),
|
||||||
|
Navigate: ({ to }: { to: string }) => `Navigate to ${to}`,
|
||||||
|
Outlet: () => null,
|
||||||
|
useNavigate: () => vi.fn(),
|
||||||
|
useLocation: () => ({ pathname: '/', search: '', hash: '', state: null, key: 'default' }),
|
||||||
|
useParams: () => ({}),
|
||||||
|
useSearchParams: () => [new URLSearchParams(), vi.fn()],
|
||||||
|
useMatch: () => null,
|
||||||
|
useResolvedPath: (to: string) => ({ pathname: to, search: '', hash: '' }),
|
||||||
|
useHref: (to: string) => to,
|
||||||
|
useOutlet: () => null,
|
||||||
|
useOutletContext: () => ({}),
|
||||||
|
}))
|
||||||
|
|
||||||
// Extend Vitest's expect with jest-dom matchers
|
// Extend Vitest's expect with jest-dom matchers
|
||||||
expect.extend(matchers)
|
expect.extend(matchers)
|
||||||
|
|
@ -26,7 +50,7 @@ Object.defineProperty(window, 'matchMedia', {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Mock IntersectionObserver
|
// Mock IntersectionObserver
|
||||||
global.IntersectionObserver = class IntersectionObserver {
|
globalThis.IntersectionObserver = class IntersectionObserver {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
disconnect() {}
|
disconnect() {}
|
||||||
observe() {}
|
observe() {}
|
||||||
|
|
@ -34,4 +58,5 @@ global.IntersectionObserver = class IntersectionObserver {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
unobserve() {}
|
unobserve() {}
|
||||||
|
|
||||||
} as any
|
} as any
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import { ReactElement } from 'react'
|
import type { ReactElement } from 'react'
|
||||||
import { render, RenderOptions } from '@testing-library/react'
|
import { render, type RenderOptions } from '@testing-library/react'
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
// Use the mocked MemoryRouter from our mock
|
||||||
|
const MemoryRouter = ({ children }: { children: React.ReactNode }) => <>{children}</>
|
||||||
|
|
||||||
// Create a custom render function that includes providers
|
// Create a custom render function that includes providers
|
||||||
const createTestQueryClient = () =>
|
const createTestQueryClient = () =>
|
||||||
|
|
@ -22,7 +25,7 @@ const AllTheProviders = ({ children }: AllTheProvidersProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={testQueryClient}>
|
<QueryClientProvider client={testQueryClient}>
|
||||||
<BrowserRouter>{children}</BrowserRouter>
|
<MemoryRouter>{children}</MemoryRouter>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -32,5 +35,7 @@ const customRender = (
|
||||||
options?: Omit<RenderOptions, 'wrapper'>
|
options?: Omit<RenderOptions, 'wrapper'>
|
||||||
) => render(ui, { wrapper: AllTheProviders, ...options })
|
) => render(ui, { wrapper: AllTheProviders, ...options })
|
||||||
|
|
||||||
export * from '@testing-library/react'
|
// Export everything from @testing-library/react
|
||||||
|
export { screen, waitFor, within, fireEvent } from '@testing-library/react'
|
||||||
|
// Export our custom render
|
||||||
export { customRender as render }
|
export { customRender as render }
|
||||||
|
|
|
||||||
9
src/test/vitest.d.ts
vendored
Normal file
9
src/test/vitest.d.ts
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import type { TestingLibraryMatchers } from '@testing-library/jest-dom/matchers'
|
||||||
|
import 'vitest'
|
||||||
|
|
||||||
|
declare module 'vitest' {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||||
|
interface Assertion<T = unknown> extends TestingLibraryMatchers<typeof expect.stringContaining, T> {}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||||
|
interface AsymmetricMatchersContaining extends TestingLibraryMatchers<ReturnType<typeof expect.stringContaining>, unknown> {}
|
||||||
|
}
|
||||||
18
src/types/activity.types.ts
Normal file
18
src/types/activity.types.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
export interface ActivityLog {
|
||||||
|
id: string
|
||||||
|
type: string
|
||||||
|
description: string
|
||||||
|
date: string
|
||||||
|
amount?: number
|
||||||
|
userId?: string
|
||||||
|
metadata?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserActivity extends ActivityLog {
|
||||||
|
userId: string
|
||||||
|
action: string
|
||||||
|
resourceType?: string
|
||||||
|
resourceId?: string
|
||||||
|
ipAddress?: string
|
||||||
|
userAgent?: string
|
||||||
|
}
|
||||||
46
src/types/analytics.types.ts
Normal file
46
src/types/analytics.types.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import type { StorageCategory } from './common.types'
|
||||||
|
|
||||||
|
export interface ApiUsageData {
|
||||||
|
date: string
|
||||||
|
requests: number
|
||||||
|
errors: number
|
||||||
|
avgResponseTime: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ErrorRateData {
|
||||||
|
date: string
|
||||||
|
errorRate: number
|
||||||
|
totalErrors: number
|
||||||
|
totalRequests: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ErrorRateSummary {
|
||||||
|
errorRate: number
|
||||||
|
errors: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StorageByUser {
|
||||||
|
userId: string
|
||||||
|
userName: string
|
||||||
|
email: string
|
||||||
|
storageUsed: number
|
||||||
|
documentCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StorageAnalytics {
|
||||||
|
totalStorage: number
|
||||||
|
usedStorage: number
|
||||||
|
availableStorage: number
|
||||||
|
byCategory?: StorageCategory[]
|
||||||
|
total?: {
|
||||||
|
size: number
|
||||||
|
files: number
|
||||||
|
}
|
||||||
|
storageByType: Array<{
|
||||||
|
type: string
|
||||||
|
size: number
|
||||||
|
count: number
|
||||||
|
}>
|
||||||
|
topUsers: StorageByUser[]
|
||||||
|
}
|
||||||
63
src/types/common.types.ts
Normal file
63
src/types/common.types.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
// Common types used across the application
|
||||||
|
|
||||||
|
export interface ApiError {
|
||||||
|
response?: {
|
||||||
|
data?: {
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryParams {
|
||||||
|
page?: number
|
||||||
|
limit?: number
|
||||||
|
search?: string
|
||||||
|
[key: string]: string | number | boolean | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Announcement {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
type: 'info' | 'warning' | 'success' | 'error'
|
||||||
|
isActive: boolean
|
||||||
|
createdAt: string
|
||||||
|
updatedAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Setting {
|
||||||
|
id: string
|
||||||
|
key: string
|
||||||
|
value: string
|
||||||
|
description?: string
|
||||||
|
isPublic: boolean
|
||||||
|
createdAt: string
|
||||||
|
updatedAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiEndpointUsage {
|
||||||
|
endpoint: string
|
||||||
|
requests: number
|
||||||
|
avgResponseTime: number
|
||||||
|
errorRate: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StorageCategory {
|
||||||
|
category: string
|
||||||
|
size: number
|
||||||
|
count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StorageData {
|
||||||
|
totalStorage: number
|
||||||
|
usedStorage: number
|
||||||
|
availableStorage: number
|
||||||
|
byCategory?: StorageCategory[]
|
||||||
|
topUsers: Array<{
|
||||||
|
userId: string
|
||||||
|
userName: string
|
||||||
|
email: string
|
||||||
|
storageUsed: number
|
||||||
|
}>
|
||||||
|
}
|
||||||
15
src/types/error.types.ts
Normal file
15
src/types/error.types.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import type { AxiosError } from 'axios'
|
||||||
|
|
||||||
|
export interface ApiErrorResponse {
|
||||||
|
message: string
|
||||||
|
statusCode?: number
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiError = AxiosError<ApiErrorResponse>
|
||||||
|
|
||||||
|
export interface LocationState {
|
||||||
|
from?: {
|
||||||
|
pathname: string
|
||||||
|
}
|
||||||
|
}
|
||||||
66
src/types/security.types.ts
Normal file
66
src/types/security.types.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
export interface SuspiciousIP {
|
||||||
|
ipAddress: string
|
||||||
|
attempts: number
|
||||||
|
lastAttempt: string
|
||||||
|
severity: 'low' | 'medium' | 'high'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SuspiciousEmail {
|
||||||
|
email: string
|
||||||
|
attempts: number
|
||||||
|
lastAttempt: string
|
||||||
|
severity: 'low' | 'medium' | 'high'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RateLimitViolation {
|
||||||
|
id: string
|
||||||
|
ipAddress: string
|
||||||
|
endpoint: string
|
||||||
|
attempts: number
|
||||||
|
requests: number
|
||||||
|
period: string
|
||||||
|
timestamp: string
|
||||||
|
userId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Session {
|
||||||
|
id: string
|
||||||
|
userId: string
|
||||||
|
ipAddress: string
|
||||||
|
userAgent: string
|
||||||
|
createdAt: string
|
||||||
|
expiresAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RateViolation {
|
||||||
|
id: string
|
||||||
|
userId?: string
|
||||||
|
ipAddress: string
|
||||||
|
endpoint: string
|
||||||
|
attempts: number
|
||||||
|
requests: number
|
||||||
|
period: string
|
||||||
|
timestamp: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FailedLogin {
|
||||||
|
id: string
|
||||||
|
email: string
|
||||||
|
ipAddress: string
|
||||||
|
userAgent: string
|
||||||
|
reason?: string
|
||||||
|
attemptedAt: string
|
||||||
|
blocked: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiKey {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
key: string
|
||||||
|
userId: string
|
||||||
|
createdAt: string
|
||||||
|
expiresAt?: string
|
||||||
|
lastUsedAt?: string
|
||||||
|
revoked?: boolean
|
||||||
|
isActive: boolean
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,12 @@ export default defineConfig({
|
||||||
environment: 'jsdom',
|
environment: 'jsdom',
|
||||||
setupFiles: './src/test/setup.ts',
|
setupFiles: './src/test/setup.ts',
|
||||||
css: true,
|
css: true,
|
||||||
|
pool: 'vmThreads',
|
||||||
|
testTimeout: 10000,
|
||||||
|
hookTimeout: 10000,
|
||||||
|
deps: {
|
||||||
|
inline: [/react-router/, /react-router-dom/],
|
||||||
|
},
|
||||||
coverage: {
|
coverage: {
|
||||||
provider: 'v8',
|
provider: 'v8',
|
||||||
reporter: ['text', 'json', 'html'],
|
reporter: ['text', 'json', 'html'],
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user