move workflow files - add prettier formatting and format
This commit is contained in:
parent
d1bf2cf2a9
commit
0c4a22748c
|
|
@ -0,0 +1,120 @@
|
|||
name: ESLint Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ '*' ]
|
||||
paths:
|
||||
- 'PROJECTS/api-security-scanner/frontend/**/*.ts'
|
||||
- 'PROJECTS/api-security-scanner/frontend/**/*.tsx'
|
||||
- 'PROJECTS/api-security-scanner/frontend/eslint.config.js'
|
||||
- 'PROJECTS/api-security-scanner/frontend/tsconfig*.json'
|
||||
- 'PROJECTS/api-security-scanner/frontend/package.json'
|
||||
- '.github/workflows/eslint-check.yml'
|
||||
|
||||
jobs:
|
||||
eslint-check:
|
||||
name: ESLint TypeScript Check
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: PROJECTS/api-security-scanner/frontend
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: PROJECTS/api-security-scanner/frontend/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Run ESLint
|
||||
id: eslint_check
|
||||
run: |
|
||||
echo "Running ESLint on TypeScript/React files..."
|
||||
if npm run lint:eslint > eslint-output.txt 2>&1; then
|
||||
echo "ESLINT_PASSED=true" >> $GITHUB_ENV
|
||||
echo "✅ No ESLint errors found!"
|
||||
echo "ERROR_COUNT=0" >> $GITHUB_ENV
|
||||
else
|
||||
echo "ESLINT_PASSED=false" >> $GITHUB_ENV
|
||||
# Count error lines (lines that contain file paths with problems)
|
||||
error_count=$(grep -c "^/" eslint-output.txt || echo "0")
|
||||
echo "ERROR_COUNT=$error_count" >> $GITHUB_ENV
|
||||
echo "⚠️ ESLint found issues in $error_count files!"
|
||||
fi
|
||||
cat eslint-output.txt
|
||||
continue-on-error: true
|
||||
|
||||
- name: Create ESLint Summary
|
||||
id: create_summary
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
{
|
||||
echo '## 🔍 ESLint Results'
|
||||
echo ''
|
||||
|
||||
if [[ "${{ env.ESLINT_PASSED }}" == "true" ]]; then
|
||||
echo '### ✅ **Perfect! No ESLint issues found** 🎉'
|
||||
echo ''
|
||||
echo 'Your TypeScript and React code follows all the coding standards perfectly!'
|
||||
echo ''
|
||||
echo '**What was checked:**'
|
||||
echo '- TypeScript strict type checking and stylistic rules'
|
||||
echo '- React component patterns and hooks usage'
|
||||
echo '- Code complexity, naming conventions, and best practices'
|
||||
echo '- Accessibility (jsx-a11y) and React Refresh compatibility'
|
||||
else
|
||||
echo '### ❌ **ESLint found issues in ${{ env.ERROR_COUNT }} files**'
|
||||
echo ''
|
||||
echo 'Please review and fix the TypeScript/React issues below:'
|
||||
echo ''
|
||||
echo '<details><summary>📋 View detailed ESLint output</summary>'
|
||||
echo ''
|
||||
echo '```'
|
||||
head -100 eslint-output.txt
|
||||
echo '```'
|
||||
echo '</details>'
|
||||
echo ''
|
||||
echo '**How to fix:**'
|
||||
echo '1. Run `cd frontend && npm run lint:eslint` locally to see the issues'
|
||||
echo '2. Fix the reported TypeScript, React, and code quality problems'
|
||||
echo '3. For auto-fixable issues: `cd frontend && npx eslint . --ext .ts,.tsx --fix`'
|
||||
echo '4. Push your changes to update this PR'
|
||||
fi
|
||||
echo ''
|
||||
echo '**Commands:**'
|
||||
echo '- `cd frontend && npm run lint:eslint` - Run ESLint'
|
||||
echo '- `cd frontend && npx eslint . --ext .ts,.tsx --fix` - Auto-fix issues'
|
||||
echo '- ESLint config: `frontend/eslint.config.js`'
|
||||
echo ''
|
||||
echo '<!-- eslint-check-comment-marker -->'
|
||||
} > eslint-report.md
|
||||
|
||||
- name: Post PR Comment
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body-path: PROJECTS/api-security-scanner/frontend/eslint-report.md
|
||||
edit-mode: replace
|
||||
|
||||
- name: Exit with proper code
|
||||
run: |
|
||||
if [[ "${{ env.ESLINT_PASSED }}" == "false" ]]; then
|
||||
echo "❌ ESLint checks failed. Please fix the issues above."
|
||||
exit 1
|
||||
else
|
||||
echo "✅ All ESLint checks passed!"
|
||||
exit 0
|
||||
fi
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
name: Lint & Type Check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ 'main' ]
|
||||
pull_request:
|
||||
branches: [ '*' ]
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Run Linters
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: PROJECTS/api-security-scanner/backend
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Cache pip dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('PROJECTS/api-security-scanner/backend/pyproject.toml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -e ".[dev]"
|
||||
|
||||
- name: Run pylint
|
||||
id: pylint
|
||||
run: |
|
||||
echo "Running pylint..."
|
||||
if pylint . > pylint-output.txt 2>&1; then
|
||||
echo "PYLINT_PASSED=true" >> $GITHUB_ENV
|
||||
echo "✅ No pylint errors found!"
|
||||
else
|
||||
echo "PYLINT_PASSED=false" >> $GITHUB_ENV
|
||||
echo "⚠️ Pylint found issues!"
|
||||
fi
|
||||
cat pylint-output.txt
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run ruff
|
||||
id: ruff
|
||||
run: |
|
||||
echo "Running ruff check..."
|
||||
if ruff check . > ruff-output.txt 2>&1; then
|
||||
echo "RUFF_PASSED=true" >> $GITHUB_ENV
|
||||
echo "✅ No ruff errors found!"
|
||||
else
|
||||
echo "RUFF_PASSED=false" >> $GITHUB_ENV
|
||||
echo "⚠️ Ruff found issues"
|
||||
fi
|
||||
cat ruff-output.txt
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run mypy
|
||||
id: mypy
|
||||
run: |
|
||||
echo "Running mypy..."
|
||||
if mypy . > mypy-output.txt 2>&1; then
|
||||
echo "MYPY_PASSED=true" >> $GITHUB_ENV
|
||||
echo "✅ No mypy errors found"
|
||||
else
|
||||
echo "MYPY_PASSED=false" >> $GITHUB_ENV
|
||||
echo "⚠️ Mypy found issues"
|
||||
fi
|
||||
cat mypy-output.txt
|
||||
continue-on-error: true
|
||||
|
||||
- name: Create Lint Summary
|
||||
id: create_summary
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
{
|
||||
echo '## 🔍 Lint & Type Check Results'
|
||||
echo ''
|
||||
|
||||
# Pylint Status
|
||||
if [[ "${{ env.PYLINT_PASSED }}" == "true" ]]; then
|
||||
echo '### ✅ Pylint: **Passed**'
|
||||
echo 'No pylint issues found.'
|
||||
else
|
||||
echo '### ⚠️ Pylint: **Issues Found**'
|
||||
echo '<details><summary>View pylint output</summary>'
|
||||
echo ''
|
||||
echo '```'
|
||||
head -100 pylint-output.txt
|
||||
echo '```'
|
||||
echo '</details>'
|
||||
fi
|
||||
echo ''
|
||||
|
||||
# Ruff Status
|
||||
if [[ "${{ env.RUFF_PASSED }}" == "true" ]]; then
|
||||
echo '### ✅ Ruff: **Passed**'
|
||||
echo 'No ruff issues found.'
|
||||
else
|
||||
echo '### ⚠️ Ruff: **Issues Found**'
|
||||
echo '<details><summary>View ruff output</summary>'
|
||||
echo ''
|
||||
echo '```'
|
||||
head -100 ruff-output.txt
|
||||
echo '```'
|
||||
echo '</details>'
|
||||
fi
|
||||
echo ''
|
||||
|
||||
# Mypy Status
|
||||
if [[ "${{ env.MYPY_PASSED }}" == "true" ]]; then
|
||||
echo '### ✅ Mypy: **Passed**'
|
||||
echo 'No mypy issues found.'
|
||||
else
|
||||
echo '### ⚠️ Mypy: **Issues Found**'
|
||||
echo '<details><summary>View mypy output</summary>'
|
||||
echo ''
|
||||
echo '```'
|
||||
head -100 mypy-output.txt
|
||||
echo '```'
|
||||
echo '</details>'
|
||||
fi
|
||||
echo ''
|
||||
|
||||
# Overall Summary
|
||||
if [[ "${{ env.PYLINT_PASSED }}" == "true" ]] && [[ "${{ env.RUFF_PASSED }}" == "true" ]] && [[ "${{ env.MYPY_PASSED }}" == "true" ]]; then
|
||||
echo '---'
|
||||
echo '### All checks passed!'
|
||||
else
|
||||
echo '---'
|
||||
echo '### Review the issues above and consider fixing them.'
|
||||
fi
|
||||
echo ''
|
||||
echo '<!-- lint-check-comment-marker -->'
|
||||
} > ../lint-report.md
|
||||
|
||||
- name: Post PR Comment
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body-path: PROJECTS/api-security-scanner/lint-report.md
|
||||
comment-marker: lint-check-comment-marker
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
name: TypeScript Type Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ '*' ]
|
||||
paths:
|
||||
- 'PROJECTS/api-security-scanner/frontend/**/*.ts'
|
||||
- 'PROJECTS/api-security-scanner/frontend/**/*.tsx'
|
||||
- 'PROJECTS/api-security-scanner/frontend/tsconfig*.json'
|
||||
- 'PROJECTS/api-security-scanner/frontend/package.json'
|
||||
- '.github/workflows/typescript-check.yml'
|
||||
|
||||
jobs:
|
||||
typescript-check:
|
||||
name: TypeScript Type Check
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: PROJECTS/api-security-scanner/frontend
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: PROJECTS/api-security-scanner/frontend/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
for i in {1..3}; do
|
||||
echo "Attempt $i of 3..."
|
||||
if npm install; then
|
||||
echo "✅ npm install succeeded"
|
||||
break
|
||||
else
|
||||
echo "⚠️ npm install failed, retrying in 10 seconds..."
|
||||
sleep 10
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Run TypeScript type checking
|
||||
id: typescript_check
|
||||
run: |
|
||||
echo "Running TypeScript type checking..."
|
||||
if npm run lint:types > typescript-output.txt 2>&1; then
|
||||
echo "TYPESCRIPT_PASSED=true" >> $GITHUB_ENV
|
||||
echo "✅ No TypeScript type errors found!"
|
||||
echo "ERROR_COUNT=0" >> $GITHUB_ENV
|
||||
else
|
||||
echo "TYPESCRIPT_PASSED=false" >> $GITHUB_ENV
|
||||
# Count error lines (lines that contain errors)
|
||||
error_count=$(grep -c "error TS" typescript-output.txt || echo "0")
|
||||
echo "ERROR_COUNT=$error_count" >> $GITHUB_ENV
|
||||
echo "⚠️ TypeScript found $error_count type errors!"
|
||||
fi
|
||||
cat typescript-output.txt
|
||||
continue-on-error: true
|
||||
|
||||
- name: Create TypeScript Summary
|
||||
id: create_summary
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
{
|
||||
echo '## 📝 TypeScript Type Check Results'
|
||||
echo ''
|
||||
|
||||
if [[ "${{ env.TYPESCRIPT_PASSED }}" == "true" ]]; then
|
||||
echo '### ✅ **Perfect! No TypeScript type errors found** 🎉'
|
||||
echo ''
|
||||
echo 'Your TypeScript code passes all strict type checking requirements!'
|
||||
echo ''
|
||||
echo '**What was checked:**'
|
||||
echo '- Strict type checking with `exactOptionalPropertyTypes`'
|
||||
echo '- No unused locals or parameters'
|
||||
echo '- Proper return types and void expressions'
|
||||
echo '- Module resolution and import/export syntax'
|
||||
else
|
||||
echo '### ❌ **TypeScript found ${{ env.ERROR_COUNT }} type errors**'
|
||||
echo ''
|
||||
echo 'Please review and fix the TypeScript type errors below:'
|
||||
echo ''
|
||||
echo '<details><summary>📋 View detailed TypeScript output</summary>'
|
||||
echo ''
|
||||
echo '```'
|
||||
head -100 typescript-output.txt
|
||||
echo '```'
|
||||
echo '</details>'
|
||||
echo ''
|
||||
echo '**How to fix:**'
|
||||
echo '1. Run `cd frontend && npm run lint:types` locally to see the type errors'
|
||||
echo '2. Fix the reported TypeScript type issues'
|
||||
echo '3. Ensure all variables have proper types and return types are explicit'
|
||||
echo '4. Push your changes to update this PR'
|
||||
fi
|
||||
echo ''
|
||||
echo '**Commands:**'
|
||||
echo '- `cd frontend && npm run lint:types` - Run TypeScript type checking'
|
||||
echo '- `cd frontend && npm run build` - Run full build with type checking'
|
||||
echo '- TypeScript config: `frontend/tsconfig.app.json`'
|
||||
echo ''
|
||||
echo '<!-- typescript-check-comment-marker -->'
|
||||
} > typescript-report.md
|
||||
|
||||
- name: Post PR Comment
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body-path: PROJECTS/api-security-scanner/frontend/typescript-report.md
|
||||
edit-mode: replace
|
||||
|
||||
- name: Exit with proper code
|
||||
run: |
|
||||
if [[ "${{ env.TYPESCRIPT_PASSED }}" == "false" ]]; then
|
||||
echo "❌ TypeScript type checking failed. Please fix the type errors above."
|
||||
exit 1
|
||||
else
|
||||
echo "✅ All TypeScript type checks passed!"
|
||||
exit 0
|
||||
fi
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
|
||||
# Other
|
||||
.DS_Store
|
||||
.env*
|
||||
.vscode/
|
||||
.idea/
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"printWidth": 77,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"quoteProps": "consistent",
|
||||
"jsxSingleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"arrowParens": "always",
|
||||
"proseWrap": "always",
|
||||
"htmlWhitespaceSensitivity": "strict",
|
||||
"endOfLine": "lf",
|
||||
"embeddedLanguageFormatting": "auto",
|
||||
"singleAttributePerLine": true,
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.tsx",
|
||||
"options": {
|
||||
"printWidth": 77,
|
||||
"jsxSingleQuote": false,
|
||||
"bracketSameLine": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": "*.ts",
|
||||
"options": {
|
||||
"printWidth": 77,
|
||||
"parser": "typescript"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.json", "*.jsonc"],
|
||||
"options": {
|
||||
"printWidth": 80,
|
||||
"trailingComma": "none"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": "*.scss",
|
||||
"options": {
|
||||
"singleQuote": false,
|
||||
"printWidth": 100
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": "*.md",
|
||||
"options": {
|
||||
"proseWrap": "always",
|
||||
"printWidth": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.yml", "*.yaml"],
|
||||
"options": {
|
||||
"singleQuote": false,
|
||||
"bracketSpacing": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ".prettierrc",
|
||||
"options": {
|
||||
"parser": "json"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": "package.json",
|
||||
"options": {
|
||||
"printWidth": 1000
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -13,7 +13,9 @@ import { router } from '@/router';
|
|||
import { useAuthStore } from '@/store/authStore';
|
||||
|
||||
const AuthInitializer = (): null => {
|
||||
const loadUserFromStorage = useAuthStore((state) => state.loadUserFromStorage);
|
||||
const loadUserFromStorage = useAuthStore(
|
||||
(state) => state.loadUserFromStorage,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadUserFromStorage();
|
||||
|
|
@ -27,7 +29,12 @@ function App(): React.ReactElement {
|
|||
<QueryClientProvider client={queryClient}>
|
||||
<AuthInitializer />
|
||||
<RouterProvider router={router} />
|
||||
<Toaster position="top-right" closeButton duration={4500} theme="dark" />
|
||||
<Toaster
|
||||
position="top-right"
|
||||
closeButton
|
||||
duration={4500}
|
||||
theme="dark"
|
||||
/>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -21,9 +21,10 @@ export const LoginForm = (): React.ReactElement => {
|
|||
|
||||
const [email, setEmail] = useState<string>('');
|
||||
const [password, setPassword] = useState<string>('');
|
||||
const [errors, setErrors] = useState<{ email?: string; password?: string }>(
|
||||
{},
|
||||
);
|
||||
const [errors, setErrors] = useState<{
|
||||
email?: string;
|
||||
password?: string;
|
||||
}>({});
|
||||
|
||||
const { mutate: login, isPending, error } = useLogin();
|
||||
|
||||
|
|
@ -49,14 +50,19 @@ export const LoginForm = (): React.ReactElement => {
|
|||
}
|
||||
};
|
||||
|
||||
const validateField = (field: 'email' | 'password', value: string): void => {
|
||||
const validateField = (
|
||||
field: 'email' | 'password',
|
||||
value: string,
|
||||
): void => {
|
||||
const result = loginSchema.safeParse({
|
||||
email: field === 'email' ? value : email,
|
||||
password: field === 'password' ? value : password,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
const fieldError = result.error.issues.find((err) => err.path[0] === field);
|
||||
const fieldError = result.error.issues.find(
|
||||
(err) => err.path[0] === field,
|
||||
);
|
||||
if (fieldError !== null && fieldError !== undefined) {
|
||||
setErrors((prev) => ({ ...prev, [field]: fieldError.message }));
|
||||
} else {
|
||||
|
|
@ -120,7 +126,10 @@ export const LoginForm = (): React.ReactElement => {
|
|||
};
|
||||
|
||||
return (
|
||||
<form className="auth-form" onSubmit={handleSubmit}>
|
||||
<form
|
||||
className="auth-form"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<div className="auth-form__header">
|
||||
<h1 className="auth-form__title">Welcome Back</h1>
|
||||
<p className="auth-form__subtitle">Sign in to your account</p>
|
||||
|
|
@ -153,18 +162,29 @@ export const LoginForm = (): React.ReactElement => {
|
|||
</div>
|
||||
|
||||
{error !== null && error !== undefined && isAxiosError(error) ? (
|
||||
<div className="auth-form__error-message" role="alert">
|
||||
{(error.response?.data as { detail?: string } | undefined)?.detail ?? 'Login failed. Please try again.'}
|
||||
<div
|
||||
className="auth-form__error-message"
|
||||
role="alert"
|
||||
>
|
||||
{(error.response?.data as { detail?: string } | undefined)
|
||||
?.detail ?? 'Login failed. Please try again.'}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<Button type="submit" isLoading={isPending} disabled={isPending}>
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={isPending}
|
||||
disabled={isPending}
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
|
||||
<p className="auth-form__link">
|
||||
Don't have an account?{' '}
|
||||
<Link to="/register" className="auth-form__link-text">
|
||||
<Link
|
||||
to="/register"
|
||||
className="auth-form__link-text"
|
||||
>
|
||||
Sign up
|
||||
</Link>
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ import './AuthForm.css';
|
|||
|
||||
export const RegisterForm = (): React.ReactElement => {
|
||||
const registerFormState = useUIStore((state) => state.registerForm);
|
||||
const setRegisterFormField = useUIStore((state) => state.setRegisterFormField);
|
||||
const setRegisterFormField = useUIStore(
|
||||
(state) => state.setRegisterFormField,
|
||||
);
|
||||
const clearRegisterForm = useUIStore((state) => state.clearRegisterForm);
|
||||
const clearExpiredData = useUIStore((state) => state.clearExpiredData);
|
||||
|
||||
|
|
@ -61,7 +63,10 @@ export const RegisterForm = (): React.ReactElement => {
|
|||
const handleConfirmPasswordChange = (value: string): void => {
|
||||
setConfirmPassword(value);
|
||||
setRegisterFormField('confirmPassword', value);
|
||||
if (errors.confirmPassword !== null && errors.confirmPassword !== undefined) {
|
||||
if (
|
||||
errors.confirmPassword !== null &&
|
||||
errors.confirmPassword !== undefined
|
||||
) {
|
||||
validateField('confirmPassword', value);
|
||||
}
|
||||
};
|
||||
|
|
@ -77,7 +82,9 @@ export const RegisterForm = (): React.ReactElement => {
|
|||
});
|
||||
|
||||
if (!result.success) {
|
||||
const fieldError = result.error.issues.find((err) => err.path[0] === field);
|
||||
const fieldError = result.error.issues.find(
|
||||
(err) => err.path[0] === field,
|
||||
);
|
||||
if (fieldError !== null && fieldError !== undefined) {
|
||||
setErrors((prev) => ({ ...prev, [field]: fieldError.message }));
|
||||
} else {
|
||||
|
|
@ -94,7 +101,9 @@ export const RegisterForm = (): React.ReactElement => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleBlur = (field: 'email' | 'password' | 'confirmPassword'): void => {
|
||||
const handleBlur = (
|
||||
field: 'email' | 'password' | 'confirmPassword',
|
||||
): void => {
|
||||
let value: string;
|
||||
if (field === 'email') {
|
||||
value = email;
|
||||
|
|
@ -153,10 +162,15 @@ export const RegisterForm = (): React.ReactElement => {
|
|||
};
|
||||
|
||||
return (
|
||||
<form className="auth-form" onSubmit={handleSubmit}>
|
||||
<form
|
||||
className="auth-form"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<div className="auth-form__header">
|
||||
<h1 className="auth-form__title">Create Account</h1>
|
||||
<p className="auth-form__subtitle">Get started with API Security Scanner</p>
|
||||
<p className="auth-form__subtitle">
|
||||
Get started with API Security Scanner
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="auth-form__fields">
|
||||
|
|
@ -198,18 +212,29 @@ export const RegisterForm = (): React.ReactElement => {
|
|||
</div>
|
||||
|
||||
{error !== null && error !== undefined && isAxiosError(error) ? (
|
||||
<div className="auth-form__error-message" role="alert">
|
||||
{(error.response?.data as { detail?: string } | undefined)?.detail ?? 'Registration failed. Please try again.'}
|
||||
<div
|
||||
className="auth-form__error-message"
|
||||
role="alert"
|
||||
>
|
||||
{(error.response?.data as { detail?: string } | undefined)
|
||||
?.detail ?? 'Registration failed. Please try again.'}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<Button type="submit" isLoading={isPending} disabled={isPending}>
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={isPending}
|
||||
disabled={isPending}
|
||||
>
|
||||
Create Account
|
||||
</Button>
|
||||
|
||||
<p className="auth-form__link">
|
||||
Already have an account?{' '}
|
||||
<Link to="/login" className="auth-form__link-text">
|
||||
<Link
|
||||
to="/login"
|
||||
className="auth-form__link-text"
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -13,12 +13,16 @@ interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
|||
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ label, error, id, ...props }, ref) => {
|
||||
const inputId = id ?? `input-${label.toLowerCase().replace(/\s+/g, '-')}`;
|
||||
const inputId =
|
||||
id ?? `input-${label.toLowerCase().replace(/\s+/g, '-')}`;
|
||||
const errorId = `${inputId}-error`;
|
||||
|
||||
return (
|
||||
<div className="input-wrapper">
|
||||
<label htmlFor={inputId} className="input-label">
|
||||
<label
|
||||
htmlFor={inputId}
|
||||
className="input-label"
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -26,11 +30,17 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|||
id={inputId}
|
||||
className={`input ${error !== null && error !== undefined ? 'input--error' : ''}`}
|
||||
aria-invalid={error !== null && error !== undefined ? true : false}
|
||||
aria-describedby={error !== null && error !== undefined ? errorId : undefined}
|
||||
aria-describedby={
|
||||
error !== null && error !== undefined ? errorId : undefined
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
{error !== null && error !== undefined ? (
|
||||
<p id={errorId} className="input-error" role="alert">
|
||||
<p
|
||||
id={errorId}
|
||||
className="input-error"
|
||||
role="alert"
|
||||
>
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -14,18 +14,26 @@ export const LoadingOverlay = ({
|
|||
tests,
|
||||
}: LoadingOverlayProps): React.ReactElement => {
|
||||
return (
|
||||
<div className="loading-overlay" role="dialog" aria-label="Scan in progress">
|
||||
<div
|
||||
className="loading-overlay"
|
||||
role="dialog"
|
||||
aria-label="Scan in progress"
|
||||
>
|
||||
<div className="loading-overlay__content">
|
||||
<div className="loading-overlay__spinner">
|
||||
<div className="spinner"></div>
|
||||
</div>
|
||||
<h2 className="loading-overlay__title">Running Security Scan</h2>
|
||||
<p className="loading-overlay__subtitle">
|
||||
Testing {tests.length} {tests.length === 1 ? 'vulnerability' : 'vulnerabilities'}
|
||||
Testing {tests.length}{' '}
|
||||
{tests.length === 1 ? 'vulnerability' : 'vulnerabilities'}
|
||||
</p>
|
||||
<div className="loading-overlay__tests">
|
||||
{tests.map((test) => (
|
||||
<div key={test} className="loading-overlay__test">
|
||||
<div
|
||||
key={test}
|
||||
className="loading-overlay__test"
|
||||
>
|
||||
{TEST_TYPE_LABELS[test]}
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,12 @@ export const ProtectedRoute = ({
|
|||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/login" replace />;
|
||||
return (
|
||||
<Navigate
|
||||
to="/login"
|
||||
replace
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return children as React.ReactElement;
|
||||
|
|
|
|||
|
|
@ -138,68 +138,82 @@ export const NewScanForm = (): React.ReactElement => {
|
|||
return (
|
||||
<>
|
||||
{isPending ? <LoadingOverlay tests={selectedTests} /> : null}
|
||||
<form className="scan-form" onSubmit={handleSubmit}>
|
||||
<div className="scan-form__fields">
|
||||
<Input
|
||||
label="Target URL"
|
||||
type="url"
|
||||
value={targetUrl}
|
||||
onChange={(e) => handleTargetUrlChange(e.target.value)}
|
||||
error={errors.targetUrl}
|
||||
placeholder="https://api.example.com/endpoint"
|
||||
required
|
||||
/>
|
||||
<form
|
||||
className="scan-form"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<div className="scan-form__fields">
|
||||
<Input
|
||||
label="Target URL"
|
||||
type="url"
|
||||
value={targetUrl}
|
||||
onChange={(e) => handleTargetUrlChange(e.target.value)}
|
||||
error={errors.targetUrl}
|
||||
placeholder="https://api.example.com/endpoint"
|
||||
required
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Auth Token (Optional)"
|
||||
type="text"
|
||||
value={authToken}
|
||||
onChange={(e) => handleAuthTokenChange(e.target.value)}
|
||||
error={errors.authToken}
|
||||
placeholder="Bearer token or API key"
|
||||
/>
|
||||
<Input
|
||||
label="Auth Token (Optional)"
|
||||
type="text"
|
||||
value={authToken}
|
||||
onChange={(e) => handleAuthTokenChange(e.target.value)}
|
||||
error={errors.authToken}
|
||||
placeholder="Bearer token or API key"
|
||||
/>
|
||||
|
||||
<div className="scan-form__field">
|
||||
<label className="scan-form__label">
|
||||
Select Tests
|
||||
{errors.testsToRun !== null && errors.testsToRun !== undefined ? (
|
||||
<span className="scan-form__error" role="alert">
|
||||
{errors.testsToRun}
|
||||
</span>
|
||||
) : null}
|
||||
</label>
|
||||
<div className="scan-form__checkboxes">
|
||||
{Object.values(SCAN_TEST_TYPES).map((test) => (
|
||||
<label key={test} className="scan-form__checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedTests.includes(test)}
|
||||
onChange={() => handleTestToggle(test)}
|
||||
className="scan-form__checkbox"
|
||||
/>
|
||||
<span>{TEST_TYPE_LABELS[test]}</span>
|
||||
</label>
|
||||
))}
|
||||
<div className="scan-form__field">
|
||||
<label className="scan-form__label">
|
||||
Select Tests
|
||||
{errors.testsToRun !== null &&
|
||||
errors.testsToRun !== undefined ? (
|
||||
<span
|
||||
className="scan-form__error"
|
||||
role="alert"
|
||||
>
|
||||
{errors.testsToRun}
|
||||
</span>
|
||||
) : null}
|
||||
</label>
|
||||
<div className="scan-form__checkboxes">
|
||||
{Object.values(SCAN_TEST_TYPES).map((test) => (
|
||||
<label
|
||||
key={test}
|
||||
className="scan-form__checkbox-label"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedTests.includes(test)}
|
||||
onChange={() => handleTestToggle(test)}
|
||||
className="scan-form__checkbox"
|
||||
/>
|
||||
<span>{TEST_TYPE_LABELS[test]}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
label="Max Requests"
|
||||
type="number"
|
||||
value={maxRequests}
|
||||
onChange={(e) => handleMaxRequestsChange(e.target.value)}
|
||||
error={errors.maxRequests}
|
||||
placeholder="50"
|
||||
min="1"
|
||||
max="50"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
label="Max Requests"
|
||||
type="number"
|
||||
value={maxRequests}
|
||||
onChange={(e) => handleMaxRequestsChange(e.target.value)}
|
||||
error={errors.maxRequests}
|
||||
placeholder="50"
|
||||
min="1"
|
||||
max="50"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button type="submit" isLoading={isPending} disabled={isPending}>
|
||||
{isPending ? 'Running Scan...' : 'Start Scan'}
|
||||
</Button>
|
||||
</form>
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={isPending}
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending ? 'Running Scan...' : 'Start Scan'}
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -65,7 +65,10 @@ export const ScansList = (): React.ReactElement => {
|
|||
const scanDate = formatDate(scan.scan_date);
|
||||
|
||||
return (
|
||||
<div key={scan.id} className="scans-list__row">
|
||||
<div
|
||||
key={scan.id}
|
||||
className="scans-list__row"
|
||||
>
|
||||
<div className="scans-list__cell">
|
||||
<span className="scans-list__url">{scan.target_url}</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -59,10 +59,15 @@ export const TestResultCard = ({
|
|||
|
||||
{result.recommendations_json.length > 0 ? (
|
||||
<div className="test-result-card__section">
|
||||
<h4 className="test-result-card__section-title">Recommendations</h4>
|
||||
<h4 className="test-result-card__section-title">
|
||||
Recommendations
|
||||
</h4>
|
||||
<ul className="test-result-card__recommendations">
|
||||
{result.recommendations_json.map((rec) => (
|
||||
<li key={`${result.id.toString()}-rec-${rec}`} className="test-result-card__recommendation">
|
||||
<li
|
||||
key={`${result.id.toString()}-rec-${rec}`}
|
||||
className="test-result-card__recommendation"
|
||||
>
|
||||
{rec}
|
||||
</li>
|
||||
))}
|
||||
|
|
@ -78,9 +83,7 @@ export const TestResultCard = ({
|
|||
className="test-result-card__evidence-toggle"
|
||||
aria-expanded={showEvidence}
|
||||
>
|
||||
<span>
|
||||
{showEvidence ? '▼' : '▶'} Technical Evidence
|
||||
</span>
|
||||
<span>{showEvidence ? '▼' : '▶'} Technical Evidence</span>
|
||||
</button>
|
||||
{showEvidence ? (
|
||||
<pre className="test-result-card__evidence">
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ export const AUTH_ENDPOINTS = {
|
|||
LOGIN: '/auth/login',
|
||||
} as const;
|
||||
|
||||
export type AuthEndpoint = (typeof AUTH_ENDPOINTS)[keyof typeof AUTH_ENDPOINTS];
|
||||
export type AuthEndpoint =
|
||||
(typeof AUTH_ENDPOINTS)[keyof typeof AUTH_ENDPOINTS];
|
||||
|
||||
/**
|
||||
* Auth Error Messages
|
||||
|
|
|
|||
|
|
@ -3,10 +3,7 @@
|
|||
// ©AngelaMos | 2025
|
||||
// ===========================
|
||||
|
||||
import {
|
||||
useMutation,
|
||||
type UseMutationResult,
|
||||
} from '@tanstack/react-query';
|
||||
import { useMutation, type UseMutationResult } from '@tanstack/react-query';
|
||||
import { isAxiosError, type AxiosError } from 'axios';
|
||||
import { toast } from 'sonner';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
|
@ -29,12 +26,23 @@ import { useAuthStore } from '@/store/authStore';
|
|||
|
||||
const createAuthErrorHandler = (context: string) => {
|
||||
return (error: unknown): void => {
|
||||
if (isAxiosError(error) && error.response?.data !== null && error.response?.data !== undefined) {
|
||||
if (
|
||||
isAxiosError(error) &&
|
||||
error.response?.data !== null &&
|
||||
error.response?.data !== undefined
|
||||
) {
|
||||
const errorData: unknown = error.response.data;
|
||||
|
||||
if (typeof errorData === 'object' && errorData !== null && 'detail' in errorData) {
|
||||
if (
|
||||
typeof errorData === 'object' &&
|
||||
errorData !== null &&
|
||||
'detail' in errorData
|
||||
) {
|
||||
const apiError = errorData as { detail: unknown };
|
||||
if (typeof apiError.detail === 'string' && apiError.detail.length > 0) {
|
||||
if (
|
||||
typeof apiError.detail === 'string' &&
|
||||
apiError.detail.length > 0
|
||||
) {
|
||||
toast.error(apiError.detail);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,9 @@ axiosInstance.interceptors.response.use(
|
|||
(error: AxiosError) => {
|
||||
if (error.response?.status === 401) {
|
||||
const requestUrl = error.config?.url ?? '';
|
||||
const isAuthEndpoint = requestUrl.includes('/auth/login') || requestUrl.includes('/auth/register');
|
||||
const isAuthEndpoint =
|
||||
requestUrl.includes('/auth/login') ||
|
||||
requestUrl.includes('/auth/register');
|
||||
|
||||
if (!isAuthEndpoint) {
|
||||
localStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,11 @@ import { toast } from 'sonner';
|
|||
|
||||
export const createApiErrorHandler = (context: string) => {
|
||||
return (error: unknown): void => {
|
||||
if (isAxiosError(error) && error.response?.data !== null && error.response?.data !== undefined) {
|
||||
if (
|
||||
isAxiosError(error) &&
|
||||
error.response?.data !== null &&
|
||||
error.response?.data !== undefined
|
||||
) {
|
||||
const errorData: unknown = error.response.data;
|
||||
|
||||
if (
|
||||
|
|
@ -24,7 +28,9 @@ export const createApiErrorHandler = (context: string) => {
|
|||
}
|
||||
|
||||
const fallbackMessage =
|
||||
error instanceof Error ? error.message : `Operation failed: ${context}`;
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: `Operation failed: ${context}`;
|
||||
toast.error(fallbackMessage);
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -33,7 +33,10 @@ export const DashboardPage = (): React.ReactElement => {
|
|||
</div>
|
||||
<div className="dashboard__header-actions">
|
||||
<span className="dashboard__user-email">{user?.email}</span>
|
||||
<Button onClick={handleLogout} variant="secondary">
|
||||
<Button
|
||||
onClick={handleLogout}
|
||||
variant="secondary"
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -27,7 +27,10 @@ export const ScanResultsPage = (): React.ReactElement => {
|
|||
return (
|
||||
<div className="scan-results__error">
|
||||
<p>Failed to load scan results. Please try again.</p>
|
||||
<Link to="/" className="scan-results__back-link">
|
||||
<Link
|
||||
to="/"
|
||||
className="scan-results__back-link"
|
||||
>
|
||||
Back to Dashboard
|
||||
</Link>
|
||||
</div>
|
||||
|
|
@ -38,7 +41,10 @@ export const ScanResultsPage = (): React.ReactElement => {
|
|||
return (
|
||||
<div className="scan-results__error">
|
||||
<p>Scan not found.</p>
|
||||
<Link to="/" className="scan-results__back-link">
|
||||
<Link
|
||||
to="/"
|
||||
className="scan-results__back-link"
|
||||
>
|
||||
Back to Dashboard
|
||||
</Link>
|
||||
</div>
|
||||
|
|
@ -55,7 +61,10 @@ export const ScanResultsPage = (): React.ReactElement => {
|
|||
<div className="scan-results">
|
||||
<div className="scan-results__container">
|
||||
<header className="scan-results__header">
|
||||
<Link to="/" className="scan-results__back-link">
|
||||
<Link
|
||||
to="/"
|
||||
className="scan-results__back-link"
|
||||
>
|
||||
← Back to Dashboard
|
||||
</Link>
|
||||
<h1 className="scan-results__title">Scan Results</h1>
|
||||
|
|
@ -77,7 +86,9 @@ export const ScanResultsPage = (): React.ReactElement => {
|
|||
</span>
|
||||
</div>
|
||||
<div className="scan-results__meta-item">
|
||||
<span className="scan-results__meta-label">Vulnerabilities:</span>
|
||||
<span className="scan-results__meta-label">
|
||||
Vulnerabilities:
|
||||
</span>
|
||||
<span
|
||||
className={`scan-results__vuln-count ${
|
||||
vulnerableCount > 0
|
||||
|
|
@ -93,7 +104,10 @@ export const ScanResultsPage = (): React.ReactElement => {
|
|||
|
||||
<div className="scan-results__tests">
|
||||
{scan.test_results.map((result) => (
|
||||
<TestResultCard key={result.id} result={result} />
|
||||
<TestResultCard
|
||||
key={result.id}
|
||||
result={result}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -37,6 +37,11 @@ export const router = createBrowserRouter([
|
|||
},
|
||||
{
|
||||
path: '*',
|
||||
element: <Navigate to="/" replace />,
|
||||
element: (
|
||||
<Navigate
|
||||
to="/"
|
||||
replace
|
||||
/>
|
||||
),
|
||||
},
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -41,9 +41,18 @@ interface UIState {
|
|||
}
|
||||
|
||||
interface UIActions {
|
||||
setLoginFormField: (field: keyof Omit<LoginFormState, 'expiresAt'>, value: string) => void;
|
||||
setRegisterFormField: (field: keyof Omit<RegisterFormState, 'expiresAt'>, value: string) => void;
|
||||
setScanFormField: (field: keyof Omit<ScanFormState, 'expiresAt'>, value: string | ScanTestType[]) => void;
|
||||
setLoginFormField: (
|
||||
field: keyof Omit<LoginFormState, 'expiresAt'>,
|
||||
value: string,
|
||||
) => void;
|
||||
setRegisterFormField: (
|
||||
field: keyof Omit<RegisterFormState, 'expiresAt'>,
|
||||
value: string,
|
||||
) => void;
|
||||
setScanFormField: (
|
||||
field: keyof Omit<ScanFormState, 'expiresAt'>,
|
||||
value: string | ScanTestType[],
|
||||
) => void;
|
||||
toggleTestExpanded: (testId: number) => void;
|
||||
clearLoginForm: () => void;
|
||||
clearRegisterForm: () => void;
|
||||
|
|
@ -113,7 +122,8 @@ export const useUIStore = create<UIStore>()(
|
|||
|
||||
toggleTestExpanded: (testId): void => {
|
||||
set((state) => {
|
||||
const currentValue = state.testResults.expandedTests[testId] ?? false;
|
||||
const currentValue =
|
||||
state.testResults.expandedTests[testId] ?? false;
|
||||
state.testResults.expandedTests[testId] = !currentValue;
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -14,11 +14,7 @@ import type {
|
|||
ScanStatus,
|
||||
Severity,
|
||||
} from './scan.types';
|
||||
import {
|
||||
SCAN_TEST_TYPES,
|
||||
SCAN_STATUS,
|
||||
SEVERITY,
|
||||
} from '@/config/constants';
|
||||
import { SCAN_TEST_TYPES, SCAN_STATUS, SEVERITY } from '@/config/constants';
|
||||
|
||||
export const isValidLoginResponse = (
|
||||
data: unknown,
|
||||
|
|
@ -89,7 +85,9 @@ const isValidTestResult = (data: unknown): data is TestResult => {
|
|||
typeof obj.evidence_json === 'object' &&
|
||||
obj.evidence_json !== null &&
|
||||
Array.isArray(obj.recommendations_json) &&
|
||||
obj.recommendations_json.every((rec: unknown) => typeof rec === 'string') &&
|
||||
obj.recommendations_json.every(
|
||||
(rec: unknown) => typeof rec === 'string',
|
||||
) &&
|
||||
typeof obj.created_at === 'string'
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue