Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
U
upgrade-data-crawler-be
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ThinhNC
upgrade-data-crawler-be
Commits
1bf2ac6c
Commit
1bf2ac6c
authored
Sep 03, 2026
by
ThinhNC
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: resolve P0/P1 audit findings for security, architecture, and race conditions
parent
c168a47c
Changes
48
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
48 changed files
with
879 additions
and
342 deletions
+879
-342
latest-audit.md
docs/audits/latest-audit.md
+198
-85
eslint.config.js
eslint.config.js
+23
-0
jest.setup.ts
jest.setup.ts
+1
-0
app.ts
src/app.ts
+1
-1
crawl-page-status.constant.ts
src/common/constants/crawl-page-status.constant.ts
+13
-0
export-type.constant.ts
src/common/constants/export-type.constant.ts
+8
-0
index.ts
src/common/constants/index.ts
+2
-0
role.constant.ts
src/common/constants/role.constant.ts
+1
-0
webhook.constant.ts
src/common/constants/webhook.constant.ts
+14
-0
data-contract.helper.ts
src/common/helpers/data-contract.helper.ts
+1
-25
database.types.ts
src/common/types/database.types.ts
+14
-0
express.d.ts
src/common/types/express.d.ts
+1
-1
swagger.json
src/docs/swagger.json
+318
-85
api-key.dto.ts
src/modules/api-keys/api-key.dto.ts
+1
-1
api-key.service.ts
src/modules/api-keys/api-key.service.ts
+1
-1
auth.service.test.ts
src/modules/auth/__tests__/auth.service.test.ts
+85
-0
auth.controller.ts
src/modules/auth/auth.controller.ts
+7
-15
auth.service.ts
src/modules/auth/auth.service.ts
+7
-5
change-detection.service.ts
src/modules/change-detection/change-detection.service.ts
+1
-1
crawl-asset.repository.ts
src/modules/crawl-assets/crawl-asset.repository.ts
+9
-0
crawl-asset.service.ts
src/modules/crawl-assets/crawl-asset.service.ts
+13
-1
crawl-export.dto.ts
src/modules/crawl-exports/crawl-export.dto.ts
+4
-1
crawl-job.service.test.ts
src/modules/crawl-jobs/__tests__/crawl-job.service.test.ts
+32
-12
crawl-job.controller.ts
src/modules/crawl-jobs/crawl-job.controller.ts
+20
-26
crawl-job.repository.ts
src/modules/crawl-jobs/crawl-job.repository.ts
+12
-17
crawl-job.route.ts
src/modules/crawl-jobs/crawl-job.route.ts
+2
-0
crawl-job.service.ts
src/modules/crawl-jobs/crawl-job.service.ts
+21
-4
crawl-job.validation.ts
src/modules/crawl-jobs/crawl-job.validation.ts
+7
-0
crawl-page-processor.service.ts
src/modules/crawl-pages/crawl-page-processor.service.ts
+1
-1
crawl-page.dto.ts
src/modules/crawl-pages/crawl-page.dto.ts
+1
-1
crawl-page.repository.ts
src/modules/crawl-pages/crawl-page.repository.ts
+2
-0
crawl-page.service.ts
src/modules/crawl-pages/crawl-page.service.ts
+1
-1
crawl-page.validation.ts
src/modules/crawl-pages/crawl-page.validation.ts
+2
-2
crawl-schedule.validation.ts
src/modules/crawl-schedules/crawl-schedule.validation.ts
+18
-28
base-export.service.ts
src/modules/exports/base-export.service.ts
+1
-1
csv-export.service.ts
src/modules/exports/csv-export.service.ts
+5
-1
export.service.ts
src/modules/exports/export.service.ts
+1
-1
json-export.service.ts
src/modules/exports/json-export.service.ts
+5
-1
markdown-export.service.ts
src/modules/exports/markdown-export.service.ts
+1
-1
xlsx-export.service.ts
src/modules/exports/xlsx-export.service.ts
+1
-1
zip-export.service.ts
src/modules/exports/zip-export.service.ts
+1
-1
user.repository.ts
src/modules/users/user.repository.ts
+2
-1
user.service.ts
src/modules/users/user.service.ts
+1
-1
user.validation.ts
src/modules/users/user.validation.ts
+8
-8
webhook-config.service.ts
src/modules/webhooks/webhook-config.service.ts
+1
-1
webhook-delivery.service.ts
src/modules/webhooks/webhook-delivery.service.ts
+1
-1
webhook.validation.ts
src/modules/webhooks/webhook.validation.ts
+9
-5
crawl.worker.processor.ts
src/queues/crawl.worker.processor.ts
+0
-4
No files found.
docs/audits/latest-audit.md
View file @
1bf2ac6c
This diff is collapsed.
Click to expand it.
eslint.config.js
View file @
1bf2ac6c
...
@@ -25,6 +25,29 @@ export default [
...
@@ -25,6 +25,29 @@ export default [
],
],
"no-console"
:
"off"
,
"no-console"
:
"off"
,
"@typescript-eslint/no-explicit-any"
:
"warn"
,
"@typescript-eslint/no-explicit-any"
:
"warn"
,
"no-restricted-imports"
:
[
"error"
,
{
paths
:
[
{
name
:
"@prisma/client"
,
message
:
"Importing directly from @prisma/client is forbidden outside repositories and database.types.ts (Rule: AGENTS.md). Use src/common/constants or src/common/types/database.types."
,
},
],
},
],
},
},
{
files
:
[
"src/**/*.repository.ts"
,
"src/database/prisma.client.ts"
,
"src/common/types/database.types.ts"
,
"src/**/__tests__/**/*.ts"
,
],
rules
:
{
"no-restricted-imports"
:
"off"
,
},
},
},
},
];
];
jest.setup.ts
View file @
1bf2ac6c
...
@@ -7,3 +7,4 @@ process.env.JWT_REFRESH_SECRET =
...
@@ -7,3 +7,4 @@ process.env.JWT_REFRESH_SECRET =
process
.
env
.
WEBHOOK_ENCRYPTION_KEY
=
process
.
env
.
WEBHOOK_ENCRYPTION_KEY
=
process
.
env
.
WEBHOOK_ENCRYPTION_KEY
||
process
.
env
.
WEBHOOK_ENCRYPTION_KEY
||
"abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
;
"abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
;
process
.
env
.
REDIS_ENABLED
=
"false"
;
src/app.ts
View file @
1bf2ac6c
...
@@ -40,7 +40,7 @@ app.use(
...
@@ -40,7 +40,7 @@ app.use(
maxAge
:
86400
,
maxAge
:
86400
,
}),
}),
);
);
app
.
use
(
morgan
(
"dev"
));
app
.
use
(
morgan
(
envConfig
.
nodeEnv
===
"production"
?
"combined"
:
"dev"
));
app
.
use
(
cookieParser
());
app
.
use
(
cookieParser
());
app
.
use
(
express
.
json
());
app
.
use
(
express
.
json
());
app
.
use
(
express
.
urlencoded
({
extended
:
true
}));
app
.
use
(
express
.
urlencoded
({
extended
:
true
}));
...
...
src/common/constants/crawl-page-status.constant.ts
0 → 100644
View file @
1bf2ac6c
export
const
CRAWL_PAGE_STATUS
=
{
PENDING
:
"PENDING"
,
SUCCESS
:
"SUCCESS"
,
FAILED
:
"FAILED"
,
BLOCKED
:
"BLOCKED"
,
REQUIRES_LOGIN
:
"REQUIRES_LOGIN"
,
CAPTCHA_DETECTED
:
"CAPTCHA_DETECTED"
,
PAYWALL_DETECTED
:
"PAYWALL_DETECTED"
,
TIMEOUT
:
"TIMEOUT"
,
SKIPPED
:
"SKIPPED"
,
}
as
const
;
export
type
CrawlPageStatus
=
keyof
typeof
CRAWL_PAGE_STATUS
;
src/common/constants/export-type.constant.ts
View file @
1bf2ac6c
...
@@ -16,3 +16,11 @@ export const EXPORT_MIME_TYPES: Record<ExportType, string> = {
...
@@ -16,3 +16,11 @@ export const EXPORT_MIME_TYPES: Record<ExportType, string> = {
MARKDOWN
:
"application/zip"
,
MARKDOWN
:
"application/zip"
,
ZIP
:
"application/zip"
,
ZIP
:
"application/zip"
,
};
};
export
const
EXPORT_STATUS
=
{
PENDING
:
"PENDING"
,
PROCESSING
:
"PROCESSING"
,
COMPLETED
:
"COMPLETED"
,
FAILED
:
"FAILED"
,
}
as
const
;
export
type
ExportStatus
=
keyof
typeof
EXPORT_STATUS
;
src/common/constants/index.ts
View file @
1bf2ac6c
...
@@ -8,3 +8,5 @@ export * from "./timezone.constant";
...
@@ -8,3 +8,5 @@ export * from "./timezone.constant";
export
*
from
"./crawl-mode.constant"
;
export
*
from
"./crawl-mode.constant"
;
export
*
from
"./schedule-frequency.constant"
;
export
*
from
"./schedule-frequency.constant"
;
export
*
from
"./asset-type.constant"
;
export
*
from
"./asset-type.constant"
;
export
*
from
"./crawl-page-status.constant"
;
export
*
from
"./webhook.constant"
;
src/common/constants/role.constant.ts
View file @
1bf2ac6c
...
@@ -5,3 +5,4 @@ export const ROLES = {
...
@@ -5,3 +5,4 @@ export const ROLES = {
}
as
const
;
}
as
const
;
export
type
Role
=
keyof
typeof
ROLES
;
export
type
Role
=
keyof
typeof
ROLES
;
export
type
UserRole
=
Role
;
src/common/constants/webhook.constant.ts
0 → 100644
View file @
1bf2ac6c
export
const
WEBHOOK_DELIVERY_STATUS
=
{
PENDING
:
"PENDING"
,
SUCCESS
:
"SUCCESS"
,
FAILED
:
"FAILED"
,
}
as
const
;
export
type
WebhookDeliveryStatus
=
keyof
typeof
WEBHOOK_DELIVERY_STATUS
;
export
const
WEBHOOK_EVENT
=
{
JOB_COMPLETED
:
"job.completed"
,
JOB_FAILED
:
"job.failed"
,
}
as
const
;
export
type
WebhookEvent
=
(
typeof
WEBHOOK_EVENT
)[
keyof
typeof
WEBHOOK_EVENT
];
src/common/helpers/data-contract.helper.ts
View file @
1bf2ac6c
import
crypto
from
"crypto"
;
import
crypto
from
"crypto"
;
import
{
CrawlPage
,
CrawlAsset
}
from
"
@prisma/client
"
;
import
{
CrawlPage
,
CrawlAsset
}
from
"
../types/database.types
"
;
import
{
import
{
CrawlPageRecord
,
CrawlPageRecord
,
DataQualityWarning
,
DataQualityWarning
,
...
@@ -15,10 +15,6 @@ import {
...
@@ -15,10 +15,6 @@ import {
DATA_CONTRACT_HASH_ALGORITHM
,
DATA_CONTRACT_HASH_ALGORITHM
,
}
from
"../constants/data-contract.constant"
;
}
from
"../constants/data-contract.constant"
;
// ─────────────────────────────────────────────
// URL Normalization
// ─────────────────────────────────────────────
/**
/**
* Normalize một URL để phục vụ deduplicate và so sánh.
* Normalize một URL để phục vụ deduplicate và so sánh.
* Các bước:
* Các bước:
...
@@ -66,10 +62,6 @@ export function normalizeUrl(rawUrl: string): string {
...
@@ -66,10 +62,6 @@ export function normalizeUrl(rawUrl: string): string {
}
}
}
}
// ─────────────────────────────────────────────
// Text Utilities
// ─────────────────────────────────────────────
/**
/**
* Strip toàn bộ Markdown syntax, trả về plain text thuần.
* Strip toàn bộ Markdown syntax, trả về plain text thuần.
* Phạm vi: heading, bold/italic, inline code, code block, link, image, blockquote, HR.
* Phạm vi: heading, bold/italic, inline code, code block, link, image, blockquote, HR.
...
@@ -234,10 +226,6 @@ export function hashContent(text: string): string | null {
...
@@ -234,10 +226,6 @@ export function hashContent(text: string): string | null {
.
digest
(
"hex"
);
.
digest
(
"hex"
);
}
}
// ─────────────────────────────────────────────
// Data Quality
// ─────────────────────────────────────────────
/**
/**
* Tính điểm chất lượng dữ liệu (0–100) của một page dựa trên các tiêu chí:
* Tính điểm chất lượng dữ liệu (0–100) của một page dựa trên các tiêu chí:
* - Có mainContent : +40 điểm
* - Có mainContent : +40 điểm
...
@@ -295,10 +283,6 @@ export function detectWarnings(params: {
...
@@ -295,10 +283,6 @@ export function detectWarnings(params: {
return
warnings
;
return
warnings
;
}
}
// ─────────────────────────────────────────────
// Asset Transformation
// ─────────────────────────────────────────────
/**
/**
* Chuyển đổi danh sách CrawlAsset sang LinkRecord[].
* Chuyển đổi danh sách CrawlAsset sang LinkRecord[].
* Phân loại internal/external dựa vào domain của startUrl (job's domain).
* Phân loại internal/external dựa vào domain của startUrl (job's domain).
...
@@ -361,10 +345,6 @@ function inferImageType(imageUrl: string): string {
...
@@ -361,10 +345,6 @@ function inferImageType(imageUrl: string): string {
}
}
}
}
// ─────────────────────────────────────────────
// Main Transformer
// ─────────────────────────────────────────────
export
interface
TransformPageOptions
{
export
interface
TransformPageOptions
{
page
:
CrawlPage
&
{
normalizedUrl
?:
string
|
null
};
page
:
CrawlPage
&
{
normalizedUrl
?:
string
|
null
};
assets
:
CrawlAsset
[];
assets
:
CrawlAsset
[];
...
@@ -452,10 +432,6 @@ export function transformPageToRecord(
...
@@ -452,10 +432,6 @@ export function transformPageToRecord(
};
};
}
}
// ─────────────────────────────────────────────
// Envelope Builder
// ─────────────────────────────────────────────
/**
/**
* Đóng gói danh sách CrawlPageRecord vào PagesJsonEnvelope để ghi ra pages.json.
* Đóng gói danh sách CrawlPageRecord vào PagesJsonEnvelope để ghi ra pages.json.
*/
*/
...
...
src/common/types/database.types.ts
0 → 100644
View file @
1bf2ac6c
export
type
{
User
,
CrawlJob
,
CrawlPage
,
CrawlAsset
,
CrawlExport
,
CrawlJobLog
,
ApiKey
,
WebhookConfig
,
WebhookDelivery
,
AuditLog
,
RefreshToken
,
Prisma
,
}
from
"@prisma/client"
;
src/common/types/express.d.ts
View file @
1bf2ac6c
import
{
UserRole
}
from
"
@prisma/clie
nt"
;
import
{
UserRole
}
from
"
../constants/role.consta
nt"
;
declare
global
{
declare
global
{
namespace
Express
{
namespace
Express
{
...
...
src/docs/swagger.json
View file @
1bf2ac6c
This diff is collapsed.
Click to expand it.
src/modules/api-keys/api-key.dto.ts
View file @
1bf2ac6c
import
type
{
ApiKey
}
from
"
@prisma/client
"
;
import
type
{
ApiKey
}
from
"
../../common/types/database.types
"
;
export
type
PublicApiKey
=
Omit
<
ApiKey
,
"keyHash"
>
;
export
type
PublicApiKey
=
Omit
<
ApiKey
,
"keyHash"
>
;
...
...
src/modules/api-keys/api-key.service.ts
View file @
1bf2ac6c
...
@@ -2,7 +2,7 @@ import crypto from "crypto";
...
@@ -2,7 +2,7 @@ import crypto from "crypto";
import
{
ApiKeyRepository
}
from
"./api-key.repository"
;
import
{
ApiKeyRepository
}
from
"./api-key.repository"
;
import
{
AppError
}
from
"../../common/errors/app-error"
;
import
{
AppError
}
from
"../../common/errors/app-error"
;
import
{
ERROR_CODE
}
from
"../../common/errors/error-code"
;
import
{
ERROR_CODE
}
from
"../../common/errors/error-code"
;
import
type
{
ApiKey
}
from
"
@prisma/client
"
;
import
type
{
ApiKey
}
from
"
../../common/types/database.types
"
;
import
{
import
{
toPublicApiKey
,
toPublicApiKey
,
type
CreatedApiKey
,
type
CreatedApiKey
,
...
...
src/modules/auth/__tests__/auth.service.test.ts
View file @
1bf2ac6c
...
@@ -170,4 +170,89 @@ describe("AuthService registration mail failures", () => {
...
@@ -170,4 +170,89 @@ describe("AuthService registration mail failures", () => {
expect
(
mailService
.
sendVerificationEmail
).
toHaveBeenCalledTimes
(
1
);
expect
(
mailService
.
sendVerificationEmail
).
toHaveBeenCalledTimes
(
1
);
expect
(
repository
.
createUser
).
not
.
toHaveBeenCalled
();
expect
(
repository
.
createUser
).
not
.
toHaveBeenCalled
();
});
});
describe
(
"AuthService forgotPassword security"
,
()
=>
{
const
activeUser
=
{
id
:
"user-active"
,
email
:
"active@example.com"
,
fullName
:
"Active User"
,
role
:
"CRAWLER_USER"
,
isActive
:
true
,
passwordHash
:
"hash"
,
createdAt
:
new
Date
(),
};
it
(
"sends password reset email internally and returns only { success: true } without leaking token or userId"
,
async
()
=>
{
const
service
=
new
AuthService
();
const
repository
=
{
findByEmail
:
jest
.
fn
().
mockResolvedValue
(
activeUser
),
};
const
mailService
=
{
sendPasswordResetEmail
:
jest
.
fn
().
mockResolvedValue
(
undefined
),
};
const
mutableService
=
service
as
unknown
as
{
repository
:
typeof
repository
;
mailService
:
typeof
mailService
;
};
mutableService
.
repository
=
repository
;
mutableService
.
mailService
=
mailService
;
const
result
=
await
service
.
forgotPassword
({
email
:
activeUser
.
email
});
expect
(
result
).
toEqual
({
success
:
true
});
expect
((
result
as
Record
<
string
,
unknown
>
).
resetToken
).
toBeUndefined
();
expect
((
result
as
Record
<
string
,
unknown
>
).
userId
).
toBeUndefined
();
expect
(
mailService
.
sendPasswordResetEmail
).
toHaveBeenCalledTimes
(
1
);
expect
(
mailService
.
sendPasswordResetEmail
).
toHaveBeenCalledWith
(
activeUser
.
email
,
expect
.
any
(
String
),
);
});
it
(
"returns { success: true } and does not call mail service if user is not found"
,
async
()
=>
{
const
service
=
new
AuthService
();
const
repository
=
{
findByEmail
:
jest
.
fn
().
mockResolvedValue
(
null
),
};
const
mailService
=
{
sendPasswordResetEmail
:
jest
.
fn
(),
};
const
mutableService
=
service
as
unknown
as
{
repository
:
typeof
repository
;
mailService
:
typeof
mailService
;
};
mutableService
.
repository
=
repository
;
mutableService
.
mailService
=
mailService
;
const
result
=
await
service
.
forgotPassword
({
email
:
"nonexistent@example.com"
,
});
expect
(
result
).
toEqual
({
success
:
true
});
expect
(
mailService
.
sendPasswordResetEmail
).
not
.
toHaveBeenCalled
();
});
it
(
"returns { success: true } and does not call mail service if user is inactive"
,
async
()
=>
{
const
service
=
new
AuthService
();
const
repository
=
{
findByEmail
:
jest
.
fn
()
.
mockResolvedValue
({
...
activeUser
,
isActive
:
false
}),
};
const
mailService
=
{
sendPasswordResetEmail
:
jest
.
fn
(),
};
const
mutableService
=
service
as
unknown
as
{
repository
:
typeof
repository
;
mailService
:
typeof
mailService
;
};
mutableService
.
repository
=
repository
;
mutableService
.
mailService
=
mailService
;
const
result
=
await
service
.
forgotPassword
({
email
:
activeUser
.
email
});
expect
(
result
).
toEqual
({
success
:
true
});
expect
(
mailService
.
sendPasswordResetEmail
).
not
.
toHaveBeenCalled
();
});
});
});
});
src/modules/auth/auth.controller.ts
View file @
1bf2ac6c
...
@@ -283,22 +283,14 @@ export class AuthController {
...
@@ -283,22 +283,14 @@ export class AuthController {
forgotPassword
=
async
(
req
:
Request
,
res
:
Response
,
next
:
NextFunction
)
=>
{
forgotPassword
=
async
(
req
:
Request
,
res
:
Response
,
next
:
NextFunction
)
=>
{
try
{
try
{
const
forgotPasswordDto
:
ForgotPasswordDto
=
req
.
body
;
const
forgotPasswordDto
:
ForgotPasswordDto
=
req
.
body
;
const
result
=
await
this
.
service
.
forgotPassword
(
forgotPasswordDto
);
await
this
.
service
.
forgotPassword
(
forgotPasswordDto
);
if
(
result
.
userId
&&
result
.
resetToken
)
{
await
this
.
auditLogService
.
log
({
await
this
.
auditLogService
.
log
({
action
:
AUDIT_ACTIONS
.
FORGOT_PASSWORD
,
userId
:
result
.
userId
,
ipAddress
:
req
.
ip
,
action
:
AUDIT_ACTIONS
.
FORGOT_PASSWORD
,
userAgent
:
req
.
headers
[
"user-agent"
]
as
string
,
ipAddress
:
req
.
ip
,
details
:
{
email
:
forgotPasswordDto
.
email
},
userAgent
:
req
.
headers
[
"user-agent"
]
as
string
,
});
details
:
{
email
:
forgotPasswordDto
.
email
},
});
await
this
.
mailService
.
sendPasswordResetEmail
(
forgotPasswordDto
.
email
,
result
.
resetToken
,
);
}
res
.
json
({
res
.
json
({
success
:
true
,
success
:
true
,
...
...
src/modules/auth/auth.service.ts
View file @
1bf2ac6c
...
@@ -502,9 +502,7 @@ export class AuthService {
...
@@ -502,9 +502,7 @@ export class AuthService {
};
};
}
}
async
forgotPassword
(
async
forgotPassword
(
data
:
ForgotPasswordDto
):
Promise
<
{
success
:
boolean
}
>
{
data
:
ForgotPasswordDto
,
):
Promise
<
{
success
:
boolean
;
resetToken
?:
string
;
userId
?:
string
}
>
{
const
{
email
}
=
data
;
const
{
email
}
=
data
;
const
user
=
await
this
.
repository
.
findByEmail
(
email
);
const
user
=
await
this
.
repository
.
findByEmail
(
email
);
...
@@ -517,10 +515,14 @@ export class AuthService {
...
@@ -517,10 +515,14 @@ export class AuthService {
expiresIn
:
"15m"
,
expiresIn
:
"15m"
,
});
});
try
{
await
this
.
mailService
.
sendPasswordResetEmail
(
user
.
email
,
resetToken
);
}
catch
(
error
:
unknown
)
{
console
.
error
(
"[Mail] Password reset delivery failed:"
,
error
);
}
return
{
return
{
success
:
true
,
success
:
true
,
resetToken
,
userId
:
user
.
id
,
};
};
}
}
...
...
src/modules/change-detection/change-detection.service.ts
View file @
1bf2ac6c
import
fs
from
"fs"
;
import
fs
from
"fs"
;
import
{
CrawlJob
,
Prisma
}
from
"
@prisma/client
"
;
import
{
CrawlJob
,
Prisma
}
from
"
../../common/types/database.types
"
;
import
{
CrawlJobRepository
}
from
"../crawl-jobs/crawl-job.repository"
;
import
{
CrawlJobRepository
}
from
"../crawl-jobs/crawl-job.repository"
;
import
{
import
{
DiffReportEnvelope
,
DiffReportEnvelope
,
...
...
src/modules/crawl-assets/crawl-asset.repository.ts
View file @
1bf2ac6c
...
@@ -57,6 +57,15 @@ export class CrawlAssetRepository {
...
@@ -57,6 +57,15 @@ export class CrawlAssetRepository {
});
});
}
}
countByJobId
(
jobId
:
string
,
assetType
?:
AssetType
)
{
return
prisma
.
crawlAsset
.
count
({
where
:
{
crawlJobId
:
jobId
,
...(
assetType
?
{
assetType
}
:
{}),
},
});
}
findAssetsForJsonExport
(
jobId
:
string
)
{
findAssetsForJsonExport
(
jobId
:
string
)
{
return
prisma
.
crawlAsset
.
findMany
({
return
prisma
.
crawlAsset
.
findMany
({
where
:
{
crawlJobId
:
jobId
},
where
:
{
crawlJobId
:
jobId
},
...
...
src/modules/crawl-assets/crawl-asset.service.ts
View file @
1bf2ac6c
...
@@ -10,7 +10,19 @@ export class CrawlAssetService {
...
@@ -10,7 +10,19 @@ export class CrawlAssetService {
page
=
1
,
page
=
1
,
limit
=
50
,
limit
=
50
,
)
{
)
{
return
this
.
repository
.
findByJobId
(
jobId
,
assetType
,
page
,
limit
);
const
safeLimit
=
Math
.
min
(
Math
.
max
(
1
,
limit
),
500
);
const
safePage
=
Math
.
max
(
1
,
page
);
const
[
items
,
total
]
=
await
Promise
.
all
([
this
.
repository
.
findByJobId
(
jobId
,
assetType
,
safePage
,
safeLimit
),
this
.
repository
.
countByJobId
(
jobId
,
assetType
),
]);
return
{
items
,
total
,
page
:
safePage
,
limit
:
safeLimit
,
totalPages
:
Math
.
ceil
(
total
/
safeLimit
),
};
}
}
async
create
(
data
:
{
async
create
(
data
:
{
...
...
src/modules/crawl-exports/crawl-export.dto.ts
View file @
1bf2ac6c
import
{
ExportType
,
ExportStatus
}
from
"@prisma/client"
;
import
{
ExportType
,
ExportStatus
,
}
from
"../../common/constants/export-type.constant"
;
export
interface
CreateCrawlExportDto
{
export
interface
CreateCrawlExportDto
{
jobId
:
string
;
jobId
:
string
;
...
...
src/modules/crawl-jobs/__tests__/crawl-job.service.test.ts
View file @
1bf2ac6c
...
@@ -5,6 +5,7 @@ jest.mock("../../../database/prisma.client", () => ({
...
@@ -5,6 +5,7 @@ jest.mock("../../../database/prisma.client", () => ({
jest
.
mock
(
"../crawl-job.repository"
);
jest
.
mock
(
"../crawl-job.repository"
);
jest
.
mock
(
"../../users/user.repository"
);
jest
.
mock
(
"../../users/user.repository"
);
jest
.
mock
(
"../../crawl-exports/crawl-export.repository"
);
jest
.
mock
(
"../../crawl-exports/crawl-export.repository"
);
jest
.
mock
(
"../../crawl-schedules/crawl-schedule.repository"
);
jest
.
mock
(
"../../../common/helpers/url.helper"
);
jest
.
mock
(
"../../../common/helpers/url.helper"
);
jest
.
mock
(
"../../../queues/crawl.queue"
,
()
=>
({
jest
.
mock
(
"../../../queues/crawl.queue"
,
()
=>
({
crawlQueue
:
{
crawlQueue
:
{
...
@@ -15,12 +16,14 @@ jest.mock("../../../queues/crawl.queue", () => ({
...
@@ -15,12 +16,14 @@ jest.mock("../../../queues/crawl.queue", () => ({
import
{
CrawlJobService
}
from
"../crawl-job.service"
;
import
{
CrawlJobService
}
from
"../crawl-job.service"
;
import
{
CrawlJobRepository
}
from
"../crawl-job.repository"
;
import
{
CrawlJobRepository
}
from
"../crawl-job.repository"
;
import
{
UserRepository
}
from
"../../users/user.repository"
;
import
{
UserRepository
}
from
"../../users/user.repository"
;
import
{
CrawlScheduleRepository
}
from
"../../crawl-schedules/crawl-schedule.repository"
;
import
*
as
urlHelper
from
"../../../common/helpers/url.helper"
;
import
*
as
urlHelper
from
"../../../common/helpers/url.helper"
;
describe
(
"CrawlJobService"
,
()
=>
{
describe
(
"CrawlJobService"
,
()
=>
{
let
service
:
CrawlJobService
;
let
service
:
CrawlJobService
;
let
mockJobRepo
:
jest
.
Mocked
<
CrawlJobRepository
>
;
let
mockJobRepo
:
jest
.
Mocked
<
CrawlJobRepository
>
;
let
mockUserRepo
:
jest
.
Mocked
<
UserRepository
>
;
let
mockUserRepo
:
jest
.
Mocked
<
UserRepository
>
;
let
mockScheduleRepo
:
jest
.
Mocked
<
CrawlScheduleRepository
>
;
beforeEach
(()
=>
{
beforeEach
(()
=>
{
jest
.
clearAllMocks
();
jest
.
clearAllMocks
();
...
@@ -32,6 +35,13 @@ describe("CrawlJobService", () => {
...
@@ -32,6 +35,13 @@ describe("CrawlJobService", () => {
findById
:
jest
.
fn
(),
findById
:
jest
.
fn
(),
}
as
any
;
}
as
any
;
mockScheduleRepo
=
{
findById
:
jest
.
fn
().
mockResolvedValue
(
null
),
}
as
any
;
(
CrawlJobRepository
as
jest
.
Mock
).
mockReturnValue
(
mockJobRepo
);
(
CrawlScheduleRepository
as
jest
.
Mock
).
mockReturnValue
(
mockScheduleRepo
);
mockUserRepo
=
{
mockUserRepo
=
{
findById
:
jest
.
fn
().
mockResolvedValue
({
findById
:
jest
.
fn
().
mockResolvedValue
({
id
:
"user-1"
,
id
:
"user-1"
,
...
@@ -109,24 +119,34 @@ describe("CrawlJobService", () => {
...
@@ -109,24 +119,34 @@ describe("CrawlJobService", () => {
).
rejects
.
toThrow
(
"Concurrent jobs quota of 3 exceeded"
);
).
rejects
.
toThrow
(
"Concurrent jobs quota of 3 exceeded"
);
});
});
it
(
"deduplicates URLs in URL_LIST mode and validates them"
,
async
()
=>
{
it
(
"throws error when scheduleId does not belong to user"
,
async
()
=>
{
const
urls
=
[
mockScheduleRepo
.
findById
.
mockResolvedValue
({
"https://example.com/1"
,
id
:
"sched-1"
,
"https://example.com/2"
,
userId
:
"other-user"
,
"https://example.com/1"
,
}
as
any
);
];
await
expect
(
service
.
create
(
"user-1"
,
{
startUrl
:
"https://example.com"
,
scheduleId
:
"sched-1"
,
}),
).
rejects
.
toThrow
(
"Crawl schedule not found"
);
});
it
(
"attaches scheduleId when schedule belongs to user"
,
async
()
=>
{
mockScheduleRepo
.
findById
.
mockResolvedValue
({
id
:
"sched-1"
,
userId
:
"user-1"
,
}
as
any
);
await
service
.
create
(
"user-1"
,
{
await
service
.
create
(
"user-1"
,
{
mode
:
"URL_LIST
"
,
startUrl
:
"https://example.com
"
,
urls
,
scheduleId
:
"sched-1"
,
});
});
expect
(
urlHelper
.
validateUrlAsync
).
toHaveBeenCalledTimes
(
2
);
expect
(
mockJobRepo
.
create
).
toHaveBeenCalledWith
(
expect
(
mockJobRepo
.
create
).
toHaveBeenCalledWith
(
expect
.
objectContaining
({
expect
.
objectContaining
({
mode
:
"URL_LIST"
,
scheduleId
:
"sched-1"
,
startUrl
:
"https://example.com/1"
,
urls
:
[
"https://example.com/1"
,
"https://example.com/2"
],
}),
}),
);
);
});
});
...
...
src/modules/crawl-jobs/crawl-job.controller.ts
View file @
1bf2ac6c
...
@@ -8,10 +8,7 @@ import { AuditLogService } from "../audit-logs/audit-log.service";
...
@@ -8,10 +8,7 @@ import { AuditLogService } from "../audit-logs/audit-log.service";
import
{
AUDIT_ACTIONS
}
from
"../../common/constants/audit-action.constant"
;
import
{
AUDIT_ACTIONS
}
from
"../../common/constants/audit-action.constant"
;
import
{
JOB_STATUS
}
from
"../../common/constants/job-status.constant"
;
import
{
JOB_STATUS
}
from
"../../common/constants/job-status.constant"
;
import
{
CrawlAssetService
}
from
"../crawl-assets/crawl-asset.service"
;
import
{
CrawlAssetService
}
from
"../crawl-assets/crawl-asset.service"
;
import
{
import
{
AssetType
}
from
"../../common/constants/asset-type.constant"
;
AssetType
,
ASSET_TYPES
,
}
from
"../../common/constants/asset-type.constant"
;
import
{
streamStorageDownload
}
from
"../../common/storage/storage-download.helper"
;
import
{
streamStorageDownload
}
from
"../../common/storage/storage-download.helper"
;
export
class
CrawlJobController
{
export
class
CrawlJobController
{
private
readonly
service
=
new
CrawlJobService
();
private
readonly
service
=
new
CrawlJobService
();
...
@@ -137,22 +134,11 @@ export class CrawlJobController {
...
@@ -137,22 +134,11 @@ export class CrawlJobController {
try
{
try
{
await
this
.
service
.
findById
(
req
.
user
.
id
,
req
.
user
.
role
,
req
.
params
.
id
);
await
this
.
service
.
findById
(
req
.
user
.
id
,
req
.
user
.
role
,
req
.
params
.
id
);
const
VALID_ASSET_TYPES
:
readonly
string
[]
=
Object
.
values
(
ASSET_TYPES
);
const
assetType
=
req
.
query
.
assetType
as
AssetType
|
undefined
;
const
rawType
=
req
.
query
.
assetType
as
string
|
undefined
;
const
page
=
Number
(
req
.
query
.
page
)
||
1
;
if
(
rawType
&&
!
VALID_ASSET_TYPES
.
includes
(
rawType
))
{
const
limit
=
Number
(
req
.
query
.
limit
)
||
50
;
res
.
status
(
400
)
.
json
({
success
:
false
,
message
:
`Invalid assetType:
${
rawType
}
`
});
return
;
}
const
assetType
=
rawType
as
AssetType
|
undefined
;
const
page
=
Math
.
max
(
1
,
parseInt
((
req
.
query
.
page
as
string
)
||
"1"
,
10
));
const
limit
=
Math
.
min
(
Math
.
max
(
1
,
parseInt
((
req
.
query
.
limit
as
string
)
||
"50"
,
10
)),
500
,
);
const
items
=
await
this
.
assetService
.
findByJobId
(
const
result
=
await
this
.
assetService
.
findByJobId
(
req
.
params
.
id
,
req
.
params
.
id
,
assetType
,
assetType
,
page
,
page
,
...
@@ -162,8 +148,13 @@ export class CrawlJobController {
...
@@ -162,8 +148,13 @@ export class CrawlJobController {
res
.
json
({
res
.
json
({
success
:
true
,
success
:
true
,
data
:
{
data
:
{
items
,
items
:
result
.
items
,
meta
:
{
page
,
limit
},
meta
:
{
total
:
result
.
total
,
page
:
result
.
page
,
limit
:
result
.
limit
,
totalPages
:
result
.
totalPages
,
},
},
},
});
});
}
catch
(
error
)
{
}
catch
(
error
)
{
...
@@ -413,11 +404,14 @@ export class CrawlJobController {
...
@@ -413,11 +404,14 @@ export class CrawlJobController {
);
);
res
.
json
({
res
.
json
({
success
:
true
,
success
:
true
,
data
:
result
.
items
,
data
:
{
pagination
:
{
items
:
result
.
items
,
total
:
result
.
total
,
meta
:
{
page
:
result
.
page
,
total
:
result
.
total
,
limit
:
result
.
limit
,
page
:
result
.
page
,
limit
:
result
.
limit
,
totalPages
:
Math
.
ceil
(
result
.
total
/
(
result
.
limit
||
1
)),
},
},
},
});
});
}
catch
(
error
)
{
}
catch
(
error
)
{
...
...
src/modules/crawl-jobs/crawl-job.repository.ts
View file @
1bf2ac6c
...
@@ -166,26 +166,21 @@ export class CrawlJobRepository {
...
@@ -166,26 +166,21 @@ export class CrawlJobRepository {
failedPages
?:
number
;
failedPages
?:
number
;
},
},
)
{
)
{
const
currentJob
=
await
prisma
.
crawlJob
.
findUnique
({
await
prisma
.
crawlJob
.
updateMany
({
where
:
{
id
},
where
:
{
select
:
{
status
:
true
},
id
,
...(
status
!==
JOB_STATUS
.
CANCELED
?
{
status
:
{
not
:
JOB_STATUS
.
CANCELED
}
}
:
{}),
},
data
:
{
status
,
...
extra
},
});
});
if
(
return
prisma
.
crawlJob
.
findUnique
({
currentJob
?.
status
===
JOB_STATUS
.
CANCELED
&&
status
!==
JOB_STATUS
.
CANCELED
)
{
return
prisma
.
crawlJob
.
findUnique
({
where
:
{
id
},
include
:
{
exports
:
true
,
},
});
}
return
prisma
.
crawlJob
.
update
({
where
:
{
id
},
where
:
{
id
},
data
:
{
status
,
...
extra
},
include
:
{
exports
:
true
,
},
});
});
}
}
...
...
src/modules/crawl-jobs/crawl-job.route.ts
View file @
1bf2ac6c
...
@@ -6,6 +6,7 @@ import {
...
@@ -6,6 +6,7 @@ import {
createCrawlJobSchema
,
createCrawlJobSchema
,
createExportSchema
,
createExportSchema
,
listCrawlJobsQuerySchema
,
listCrawlJobsQuerySchema
,
getAssetsQuerySchema
,
}
from
"./crawl-job.validation"
;
}
from
"./crawl-job.validation"
;
import
{
crawlPageQuerySchema
}
from
"../crawl-pages/crawl-page.validation"
;
import
{
crawlPageQuerySchema
}
from
"../crawl-pages/crawl-page.validation"
;
import
{
requireRole
}
from
"../../middlewares/role.middleware"
;
import
{
requireRole
}
from
"../../middlewares/role.middleware"
;
...
@@ -105,6 +106,7 @@ router.get(
...
@@ -105,6 +106,7 @@ router.get(
"/:id/assets"
,
"/:id/assets"
,
apiKeyOrAuthMiddleware
,
apiKeyOrAuthMiddleware
,
requireRole
(
ROLES
.
ADMIN
,
ROLES
.
CRAWLER_USER
,
ROLES
.
VIEWER
),
requireRole
(
ROLES
.
ADMIN
,
ROLES
.
CRAWLER_USER
,
ROLES
.
VIEWER
),
validateQuery
(
getAssetsQuerySchema
),
controller
.
getAssets
,
controller
.
getAssets
,
);
);
router
.
get
(
router
.
get
(
...
...
src/modules/crawl-jobs/crawl-job.service.ts
View file @
1bf2ac6c
import
{
CrawlJobRepository
}
from
"./crawl-job.repository"
;
import
{
CrawlJobRepository
}
from
"./crawl-job.repository"
;
import
{
CrawlExportRepository
}
from
"../crawl-exports/crawl-export.repository"
;
import
{
CrawlExportRepository
}
from
"../crawl-exports/crawl-export.repository"
;
import
{
CrawlScheduleRepository
}
from
"../crawl-schedules/crawl-schedule.repository"
;
import
{
UserRepository
}
from
"../users/user.repository"
;
import
{
UserRepository
}
from
"../users/user.repository"
;
import
{
AppError
}
from
"../../common/errors/app-error"
;
import
{
AppError
}
from
"../../common/errors/app-error"
;
import
{
ERROR_CODE
}
from
"../../common/errors/error-code"
;
import
{
ERROR_CODE
}
from
"../../common/errors/error-code"
;
...
@@ -21,6 +22,8 @@ import { getErrorMessage } from "../../common/helpers/error-mapping.helper";
...
@@ -21,6 +22,8 @@ import { getErrorMessage } from "../../common/helpers/error-mapping.helper";
export
class
CrawlJobService
{
export
class
CrawlJobService
{
private
readonly
repository
=
new
CrawlJobRepository
();
private
readonly
repository
=
new
CrawlJobRepository
();
private
readonly
userRepository
=
new
UserRepository
();
private
readonly
userRepository
=
new
UserRepository
();
private
readonly
scheduleRepository
=
new
CrawlScheduleRepository
();
private
readonly
exportRepository
=
new
CrawlExportRepository
();
async
create
(
userId
:
string
,
payload
:
CreateCrawlJobDto
)
{
async
create
(
userId
:
string
,
payload
:
CreateCrawlJobDto
)
{
const
isUrlList
=
payload
.
mode
===
CRAWL_MODE
.
URL_LIST
;
const
isUrlList
=
payload
.
mode
===
CRAWL_MODE
.
URL_LIST
;
...
@@ -41,6 +44,22 @@ export class CrawlJobService {
...
@@ -41,6 +44,22 @@ export class CrawlJobService {
throw
new
AppError
(
"User not found"
,
404
,
ERROR_CODE
.
NOT_FOUND
);
throw
new
AppError
(
"User not found"
,
404
,
ERROR_CODE
.
NOT_FOUND
);
}
}
if
(
payload
.
scheduleId
)
{
const
schedule
=
await
this
.
scheduleRepository
.
findById
(
payload
.
scheduleId
,
);
if
(
!
schedule
||
(
user
.
role
!==
ROLES
.
ADMIN
&&
schedule
.
userId
!==
userId
)
)
{
throw
new
AppError
(
"Crawl schedule not found"
,
404
,
ERROR_CODE
.
CRAWL_SCHEDULE_NOT_FOUND
,
);
}
}
// SSRF validation with bounded concurrency for URL_LIST
// SSRF validation with bounded concurrency for URL_LIST
if
(
isUrlList
)
{
if
(
isUrlList
)
{
const
{
validateUrlAsync
}
=
const
{
validateUrlAsync
}
=
...
@@ -221,8 +240,7 @@ export class CrawlJobService {
...
@@ -221,8 +240,7 @@ export class CrawlJobService {
);
);
}
}
const
exportRepository
=
new
CrawlExportRepository
();
const
exports
=
await
this
.
exportRepository
.
findByJobId
(
jobId
);
const
exports
=
await
exportRepository
.
findByJobId
(
jobId
);
const
storage
=
StorageFactory
.
getStorageService
();
const
storage
=
StorageFactory
.
getStorageService
();
for
(
const
exportRecord
of
exports
)
{
for
(
const
exportRecord
of
exports
)
{
...
@@ -254,8 +272,7 @@ export class CrawlJobService {
...
@@ -254,8 +272,7 @@ export class CrawlJobService {
);
);
}
}
const
exportRepository
=
new
CrawlExportRepository
();
const
exports
=
await
this
.
exportRepository
.
findByJobId
(
jobId
);
const
exports
=
await
exportRepository
.
findByJobId
(
jobId
);
const
storage
=
StorageFactory
.
getStorageService
();
const
storage
=
StorageFactory
.
getStorageService
();
for
(
const
exp
of
exports
)
{
for
(
const
exp
of
exports
)
{
...
...
src/modules/crawl-jobs/crawl-job.validation.ts
View file @
1bf2ac6c
...
@@ -2,6 +2,7 @@ import { z } from "zod";
...
@@ -2,6 +2,7 @@ import { z } from "zod";
import
{
EXPORT_TYPE
}
from
"../../common/constants/export-type.constant"
;
import
{
EXPORT_TYPE
}
from
"../../common/constants/export-type.constant"
;
import
{
JOB_STATUS
}
from
"../../common/constants/job-status.constant"
;
import
{
JOB_STATUS
}
from
"../../common/constants/job-status.constant"
;
import
{
CRAWL_MODE
}
from
"../../common/constants/crawl-mode.constant"
;
import
{
CRAWL_MODE
}
from
"../../common/constants/crawl-mode.constant"
;
import
{
ASSET_TYPES
}
from
"../../common/constants"
;
export
const
createCrawlJobSchema
=
z
export
const
createCrawlJobSchema
=
z
.
object
({
.
object
({
...
@@ -67,3 +68,9 @@ export const listCrawlJobsQuerySchema = z.object({
...
@@ -67,3 +68,9 @@ export const listCrawlJobsQuerySchema = z.object({
page
:
z
.
coerce
.
number
().
int
().
min
(
1
).
optional
(),
page
:
z
.
coerce
.
number
().
int
().
min
(
1
).
optional
(),
limit
:
z
.
coerce
.
number
().
int
().
min
(
1
).
max
(
100
).
optional
(),
limit
:
z
.
coerce
.
number
().
int
().
min
(
1
).
max
(
100
).
optional
(),
});
});
export
const
getAssetsQuerySchema
=
z
.
object
({
assetType
:
z
.
nativeEnum
(
ASSET_TYPES
).
optional
(),
page
:
z
.
coerce
.
number
().
int
().
min
(
1
).
default
(
1
),
limit
:
z
.
coerce
.
number
().
int
().
min
(
1
).
max
(
500
).
default
(
50
),
});
src/modules/crawl-pages/crawl-page-processor.service.ts
View file @
1bf2ac6c
import
{
CrawlPageStatus
}
from
"
@prisma/clie
nt"
;
import
{
CrawlPageStatus
}
from
"
../../common/constants/crawl-page-status.consta
nt"
;
import
{
import
{
FirecrawlPageResult
,
FirecrawlPageResult
,
CrawlErrorItem
,
CrawlErrorItem
,
...
...
src/modules/crawl-pages/crawl-page.dto.ts
View file @
1bf2ac6c
import
{
CrawlPageStatus
}
from
"
@prisma/clie
nt"
;
import
{
CrawlPageStatus
}
from
"
../../common/constants/crawl-page-status.consta
nt"
;
export
interface
CreateCrawlPageDto
{
export
interface
CreateCrawlPageDto
{
jobId
:
string
;
jobId
:
string
;
...
...
src/modules/crawl-pages/crawl-page.repository.ts
View file @
1bf2ac6c
...
@@ -272,6 +272,7 @@ export class CrawlPageRepository {
...
@@ -272,6 +272,7 @@ export class CrawlPageRepository {
contentHash
?:
string
|
null
;
contentHash
?:
string
|
null
;
dataQualityScore
?:
number
|
null
;
dataQualityScore
?:
number
|
null
;
warnings
?:
string
[];
warnings
?:
string
[];
hasSensitiveData
?:
boolean
;
})
{
})
{
return
prisma
.
crawlPage
.
upsert
({
return
prisma
.
crawlPage
.
upsert
({
where
:
{
jobId_url
:
{
jobId
:
data
.
jobId
,
url
:
data
.
url
}
},
where
:
{
jobId_url
:
{
jobId
:
data
.
jobId
,
url
:
data
.
url
}
},
...
@@ -291,6 +292,7 @@ export class CrawlPageRepository {
...
@@ -291,6 +292,7 @@ export class CrawlPageRepository {
contentHash
:
data
.
contentHash
,
contentHash
:
data
.
contentHash
,
dataQualityScore
:
data
.
dataQualityScore
,
dataQualityScore
:
data
.
dataQualityScore
,
warnings
:
data
.
warnings
,
warnings
:
data
.
warnings
,
hasSensitiveData
:
data
.
hasSensitiveData
,
},
},
});
});
}
}
...
...
src/modules/crawl-pages/crawl-page.service.ts
View file @
1bf2ac6c
import
{
CrawlPageRepository
}
from
"./crawl-page.repository"
;
import
{
CrawlPageRepository
}
from
"./crawl-page.repository"
;
import
{
CrawlPageStatus
}
from
"
@prisma/clie
nt"
;
import
{
CrawlPageStatus
}
from
"
../../common/constants/crawl-page-status.consta
nt"
;
import
{
CrawlPageQueryDto
}
from
"./crawl-page.dto"
;
import
{
CrawlPageQueryDto
}
from
"./crawl-page.dto"
;
import
{
import
{
extractMainContent
,
extractMainContent
,
...
...
src/modules/crawl-pages/crawl-page.validation.ts
View file @
1bf2ac6c
import
{
z
}
from
"zod"
;
import
{
z
}
from
"zod"
;
import
{
C
rawlPageStatus
}
from
"@prisma/clie
nt"
;
import
{
C
RAWL_PAGE_STATUS
}
from
"../../common/constants/crawl-page-status.consta
nt"
;
const
parseBooleanQuery
=
(
val
:
unknown
)
=>
{
const
parseBooleanQuery
=
(
val
:
unknown
)
=>
{
if
(
val
===
undefined
||
val
===
null
||
val
===
""
)
return
undefined
;
if
(
val
===
undefined
||
val
===
null
||
val
===
""
)
return
undefined
;
...
@@ -15,7 +15,7 @@ const parseNumberQuery = (val: unknown) => {
...
@@ -15,7 +15,7 @@ const parseNumberQuery = (val: unknown) => {
};
};
export
const
crawlPageQuerySchema
=
z
.
object
({
export
const
crawlPageQuerySchema
=
z
.
object
({
status
:
z
.
nativeEnum
(
C
rawlPageStatus
).
optional
(),
status
:
z
.
nativeEnum
(
C
RAWL_PAGE_STATUS
).
optional
(),
statusCode
:
z
.
preprocess
(
parseNumberQuery
,
z
.
number
().
int
().
optional
()),
statusCode
:
z
.
preprocess
(
parseNumberQuery
,
z
.
number
().
int
().
optional
()),
search
:
z
.
string
().
trim
().
optional
(),
search
:
z
.
string
().
trim
().
optional
(),
...
...
src/modules/crawl-schedules/crawl-schedule.validation.ts
View file @
1bf2ac6c
...
@@ -46,20 +46,6 @@ export const createCrawlScheduleSchema = z
...
@@ -46,20 +46,6 @@ export const createCrawlScheduleSchema = z
});
});
}
}
}
}
if
(
data
.
frequency
===
SCHEDULE_FREQUENCY
.
WEEKLY
&&
data
.
dayOfWeek
===
undefined
)
{
data
.
dayOfWeek
=
0
;
// Default to Sunday
}
if
(
data
.
frequency
===
SCHEDULE_FREQUENCY
.
MONTHLY
&&
data
.
dayOfMonth
===
undefined
)
{
data
.
dayOfMonth
=
1
;
// Default to 1st of month
}
});
});
export
const
updateCrawlScheduleSchema
=
z
export
const
updateCrawlScheduleSchema
=
z
...
@@ -99,20 +85,24 @@ export const updateCrawlScheduleSchema = z
...
@@ -99,20 +85,24 @@ export const updateCrawlScheduleSchema = z
export
const
crawlScheduleQuerySchema
=
z
.
object
({
export
const
crawlScheduleQuerySchema
=
z
.
object
({
search
:
z
.
string
().
trim
().
optional
(),
search
:
z
.
string
().
trim
().
optional
(),
frequency
:
z
.
nativeEnum
(
SCHEDULE_FREQUENCY
).
optional
(),
frequency
:
z
.
nativeEnum
(
SCHEDULE_FREQUENCY
).
optional
(),
isActive
:
z
isActive
:
z
.
preprocess
((
val
)
=>
{
.
string
()
if
(
val
===
"true"
||
val
===
true
||
val
===
"1"
)
return
true
;
.
optional
()
if
(
val
===
"false"
||
val
===
false
||
val
===
"0"
)
return
false
;
.
transform
((
val
)
=>
return
undefined
;
val
===
"true"
?
true
:
val
===
"false"
?
false
:
undefined
,
},
z
.
boolean
().
optional
()),
),
page
:
z
.
coerce
.
number
().
int
().
min
(
1
).
default
(
1
),
page
:
z
limit
:
z
.
coerce
.
number
().
int
().
min
(
1
).
max
(
100
).
default
(
20
),
.
string
()
sortBy
:
z
.
optional
()
.
enum
([
.
transform
((
val
)
=>
(
val
?
parseInt
(
val
,
10
)
:
1
)),
"createdAt"
,
limit
:
z
"updatedAt"
,
.
string
()
"name"
,
"frequency"
,
"nextRunAt"
,
"lastRunAt"
,
"isActive"
,
])
.
optional
()
.
optional
()
.
transform
((
val
)
=>
(
val
?
parseInt
(
val
,
10
)
:
20
)),
.
default
(
"createdAt"
),
sortBy
:
z
.
string
().
optional
().
default
(
"createdAt"
),
order
:
z
.
enum
([
"asc"
,
"desc"
]).
optional
().
default
(
"desc"
),
order
:
z
.
enum
([
"asc"
,
"desc"
]).
optional
().
default
(
"desc"
),
});
});
src/modules/exports/base-export.service.ts
View file @
1bf2ac6c
import
{
CrawlJob
,
CrawlPage
}
from
"
@prisma/client
"
;
import
{
CrawlJob
,
CrawlPage
}
from
"
../../common/types/database.types
"
;
import
path
from
"path"
;
import
path
from
"path"
;
import
{
import
{
ensureJobExportStructure
,
ensureJobExportStructure
,
...
...
src/modules/exports/csv-export.service.ts
View file @
1bf2ac6c
import
fs
from
"fs"
;
import
fs
from
"fs"
;
import
{
CrawlAsset
,
CrawlJob
,
CrawlPage
}
from
"@prisma/client"
;
import
{
CrawlAsset
,
CrawlJob
,
CrawlPage
,
}
from
"../../common/types/database.types"
;
import
{
CrawlAssetRepository
}
from
"../crawl-assets/crawl-asset.repository"
;
import
{
CrawlAssetRepository
}
from
"../crawl-assets/crawl-asset.repository"
;
import
{
JOB_EXPORT_FILES
}
from
"../../common/constants/storage-path.constant"
;
import
{
JOB_EXPORT_FILES
}
from
"../../common/constants/storage-path.constant"
;
import
{
EXPORT_MIME_TYPES
}
from
"../../common/constants/export-type.constant"
;
import
{
EXPORT_MIME_TYPES
}
from
"../../common/constants/export-type.constant"
;
...
...
src/modules/exports/export.service.ts
View file @
1bf2ac6c
import
{
CrawlJob
,
CrawlPage
}
from
"
@prisma/client
"
;
import
{
CrawlJob
,
CrawlPage
}
from
"
../../common/types/database.types
"
;
import
{
CrawlJobRepository
}
from
"../crawl-jobs/crawl-job.repository"
;
import
{
CrawlJobRepository
}
from
"../crawl-jobs/crawl-job.repository"
;
import
{
CrawlExportRepository
}
from
"../crawl-exports/crawl-export.repository"
;
import
{
CrawlExportRepository
}
from
"../crawl-exports/crawl-export.repository"
;
import
{
JsonExportService
}
from
"./json-export.service"
;
import
{
JsonExportService
}
from
"./json-export.service"
;
...
...
src/modules/exports/json-export.service.ts
View file @
1bf2ac6c
import
fs
from
"fs"
;
import
fs
from
"fs"
;
import
{
parse
as
parseHtml
}
from
"node-html-parser"
;
import
{
parse
as
parseHtml
}
from
"node-html-parser"
;
import
{
CrawlJob
,
CrawlPage
,
CrawlAsset
}
from
"@prisma/client"
;
import
{
CrawlJob
,
CrawlPage
,
CrawlAsset
,
}
from
"../../common/types/database.types"
;
import
{
CrawlAssetRepository
}
from
"../crawl-assets/crawl-asset.repository"
;
import
{
CrawlAssetRepository
}
from
"../crawl-assets/crawl-asset.repository"
;
import
{
JOB_EXPORT_FILES
}
from
"../../common/constants/storage-path.constant"
;
import
{
JOB_EXPORT_FILES
}
from
"../../common/constants/storage-path.constant"
;
import
{
EXPORT_MIME_TYPES
}
from
"../../common/constants/export-type.constant"
;
import
{
EXPORT_MIME_TYPES
}
from
"../../common/constants/export-type.constant"
;
...
...
src/modules/exports/markdown-export.service.ts
View file @
1bf2ac6c
import
fs
from
"fs"
;
import
fs
from
"fs"
;
import
archiver
from
"archiver"
;
import
archiver
from
"archiver"
;
import
{
CrawlJob
,
CrawlPage
}
from
"
@prisma/client
"
;
import
{
CrawlJob
,
CrawlPage
}
from
"
../../common/types/database.types
"
;
import
{
JOB_EXPORT_SUBDIRS
}
from
"../../common/constants/storage-path.constant"
;
import
{
JOB_EXPORT_SUBDIRS
}
from
"../../common/constants/storage-path.constant"
;
import
{
EXPORT_MIME_TYPES
}
from
"../../common/constants/export-type.constant"
;
import
{
EXPORT_MIME_TYPES
}
from
"../../common/constants/export-type.constant"
;
import
{
import
{
...
...
src/modules/exports/xlsx-export.service.ts
View file @
1bf2ac6c
import
ExcelJS
from
"exceljs"
;
import
ExcelJS
from
"exceljs"
;
import
{
parse
as
parseHtml
}
from
"node-html-parser"
;
import
{
parse
as
parseHtml
}
from
"node-html-parser"
;
import
{
CrawlJob
,
CrawlPage
}
from
"
@prisma/client
"
;
import
{
CrawlJob
,
CrawlPage
}
from
"
../../common/types/database.types
"
;
import
{
JOB_EXPORT_FILES
}
from
"../../common/constants/storage-path.constant"
;
import
{
JOB_EXPORT_FILES
}
from
"../../common/constants/storage-path.constant"
;
import
{
EXPORT_MIME_TYPES
}
from
"../../common/constants/export-type.constant"
;
import
{
EXPORT_MIME_TYPES
}
from
"../../common/constants/export-type.constant"
;
import
{
buildJobDataFilePath
}
from
"../../common/helpers/file.helper"
;
import
{
buildJobDataFilePath
}
from
"../../common/helpers/file.helper"
;
...
...
src/modules/exports/zip-export.service.ts
View file @
1bf2ac6c
...
@@ -2,7 +2,7 @@ import fs from "fs";
...
@@ -2,7 +2,7 @@ import fs from "fs";
import
path
from
"path"
;
import
path
from
"path"
;
import
archiver
from
"archiver"
;
import
archiver
from
"archiver"
;
import
{
PassThrough
}
from
"stream"
;
import
{
PassThrough
}
from
"stream"
;
import
{
CrawlJob
,
CrawlPage
}
from
"
@prisma/client
"
;
import
{
CrawlJob
,
CrawlPage
}
from
"
../../common/types/database.types
"
;
import
{
CrawlAssetRepository
}
from
"../crawl-assets/crawl-asset.repository"
;
import
{
CrawlAssetRepository
}
from
"../crawl-assets/crawl-asset.repository"
;
import
{
import
{
JOB_EXPORT_FILES
,
JOB_EXPORT_FILES
,
...
...
src/modules/users/user.repository.ts
View file @
1bf2ac6c
...
@@ -2,6 +2,7 @@ import { prisma } from "../../database/prisma.client";
...
@@ -2,6 +2,7 @@ import { prisma } from "../../database/prisma.client";
import
{
UserRole
,
Prisma
,
User
}
from
"@prisma/client"
;
import
{
UserRole
,
Prisma
,
User
}
from
"@prisma/client"
;
import
{
UserQueryDto
}
from
"./user.dto"
;
import
{
UserQueryDto
}
from
"./user.dto"
;
import
{
envConfig
}
from
"../../config/env.config"
;
import
{
envConfig
}
from
"../../config/env.config"
;
import
{
ROLES
}
from
"../../common/constants/role.constant"
;
export
class
UserRepository
{
export
class
UserRepository
{
async
findAll
(
query
:
UserQueryDto
=
{})
{
async
findAll
(
query
:
UserQueryDto
=
{})
{
...
@@ -88,7 +89,7 @@ export class UserRepository {
...
@@ -88,7 +89,7 @@ export class UserRepository {
passwordHash
:
data
.
passwordHash
,
passwordHash
:
data
.
passwordHash
,
fullName
:
data
.
fullName
,
fullName
:
data
.
fullName
,
avatarUrl
:
data
.
avatarUrl
,
avatarUrl
:
data
.
avatarUrl
,
role
:
data
.
role
??
"CRAWLER_USER"
,
role
:
data
.
role
??
ROLES
.
CRAWLER_USER
,
maxPagesLimit
:
data
.
maxPagesLimit
??
envConfig
.
quota
.
defaultMaxPages
,
maxPagesLimit
:
data
.
maxPagesLimit
??
envConfig
.
quota
.
defaultMaxPages
,
maxJobsPerDayLimit
:
maxJobsPerDayLimit
:
data
.
maxJobsPerDayLimit
??
envConfig
.
quota
.
defaultMaxJobsPerDay
,
data
.
maxJobsPerDayLimit
??
envConfig
.
quota
.
defaultMaxJobsPerDay
,
...
...
src/modules/users/user.service.ts
View file @
1bf2ac6c
...
@@ -2,7 +2,7 @@ import bcrypt from "bcryptjs";
...
@@ -2,7 +2,7 @@ import bcrypt from "bcryptjs";
import
{
UserRepository
}
from
"./user.repository"
;
import
{
UserRepository
}
from
"./user.repository"
;
import
{
AppError
}
from
"../../common/errors/app-error"
;
import
{
AppError
}
from
"../../common/errors/app-error"
;
import
{
ERROR_CODE
}
from
"../../common/errors/error-code"
;
import
{
ERROR_CODE
}
from
"../../common/errors/error-code"
;
import
{
User
}
from
"
@prisma/client
"
;
import
{
User
}
from
"
../../common/types/database.types
"
;
import
{
ROLES
}
from
"../../common/constants/role.constant"
;
import
{
ROLES
}
from
"../../common/constants/role.constant"
;
import
{
import
{
CreateUserDto
,
CreateUserDto
,
...
...
src/modules/users/user.validation.ts
View file @
1bf2ac6c
...
@@ -11,9 +11,9 @@ export const createUserSchema = z.object({
...
@@ -11,9 +11,9 @@ export const createUserSchema = z.object({
.
or
(
z
.
literal
(
""
))
.
or
(
z
.
literal
(
""
))
.
optional
(),
.
optional
(),
role
:
z
.
nativeEnum
(
ROLES
).
optional
(),
role
:
z
.
nativeEnum
(
ROLES
).
optional
(),
maxPagesLimit
:
z
.
number
().
int
().
min
(
1
).
optional
(),
maxPagesLimit
:
z
.
number
().
int
().
min
(
1
).
max
(
100000
).
optional
(),
maxJobsPerDayLimit
:
z
.
number
().
int
().
min
(
1
).
optional
(),
maxJobsPerDayLimit
:
z
.
number
().
int
().
min
(
1
).
max
(
10000
).
optional
(),
maxConcurrentJobsLimit
:
z
.
number
().
int
().
min
(
1
).
optional
(),
maxConcurrentJobsLimit
:
z
.
number
().
int
().
min
(
1
).
max
(
100
).
optional
(),
});
});
export
const
updateUserSchema
=
z
.
object
({
export
const
updateUserSchema
=
z
.
object
({
...
@@ -26,9 +26,9 @@ export const updateUserSchema = z.object({
...
@@ -26,9 +26,9 @@ export const updateUserSchema = z.object({
.
optional
(),
.
optional
(),
isActive
:
z
.
boolean
().
optional
(),
isActive
:
z
.
boolean
().
optional
(),
role
:
z
.
nativeEnum
(
ROLES
).
optional
(),
role
:
z
.
nativeEnum
(
ROLES
).
optional
(),
maxPagesLimit
:
z
.
number
().
int
().
min
(
1
).
optional
(),
maxPagesLimit
:
z
.
number
().
int
().
min
(
1
).
max
(
100000
).
optional
(),
maxJobsPerDayLimit
:
z
.
number
().
int
().
min
(
1
).
optional
(),
maxJobsPerDayLimit
:
z
.
number
().
int
().
min
(
1
).
max
(
10000
).
optional
(),
maxConcurrentJobsLimit
:
z
.
number
().
int
().
min
(
1
).
optional
(),
maxConcurrentJobsLimit
:
z
.
number
().
int
().
min
(
1
).
max
(
100
).
optional
(),
});
});
export
const
listUsersQuerySchema
=
z
.
object
({
export
const
listUsersQuerySchema
=
z
.
object
({
...
@@ -38,6 +38,6 @@ export const listUsersQuerySchema = z.object({
...
@@ -38,6 +38,6 @@ export const listUsersQuerySchema = z.object({
.
enum
([
"true"
,
"false"
])
.
enum
([
"true"
,
"false"
])
.
transform
((
v
)
=>
v
===
"true"
)
.
transform
((
v
)
=>
v
===
"true"
)
.
optional
(),
.
optional
(),
page
:
z
.
coerce
.
number
().
int
().
min
(
1
).
optional
(
),
page
:
z
.
coerce
.
number
().
int
().
min
(
1
).
default
(
1
),
limit
:
z
.
coerce
.
number
().
int
().
min
(
1
).
max
(
100
).
optional
(
),
limit
:
z
.
coerce
.
number
().
int
().
min
(
1
).
max
(
100
).
default
(
20
),
});
});
src/modules/webhooks/webhook-config.service.ts
View file @
1bf2ac6c
import
{
WebhookConfig
}
from
"
@prisma/client
"
;
import
{
WebhookConfig
}
from
"
../../common/types/database.types
"
;
import
{
WebhookRepository
}
from
"./webhook.repository"
;
import
{
WebhookRepository
}
from
"./webhook.repository"
;
import
{
encrypt
}
from
"./webhook-crypto.helper"
;
import
{
encrypt
}
from
"./webhook-crypto.helper"
;
import
{
AppError
}
from
"../../common/errors/app-error"
;
import
{
AppError
}
from
"../../common/errors/app-error"
;
...
...
src/modules/webhooks/webhook-delivery.service.ts
View file @
1bf2ac6c
import
{
WebhookDelivery
,
Prisma
}
from
"
@prisma/client
"
;
import
{
WebhookDelivery
,
Prisma
}
from
"
../../common/types/database.types
"
;
import
{
WebhookRepository
}
from
"./webhook.repository"
;
import
{
WebhookRepository
}
from
"./webhook.repository"
;
import
{
decrypt
,
signPayload
}
from
"./webhook-crypto.helper"
;
import
{
decrypt
,
signPayload
}
from
"./webhook-crypto.helper"
;
import
{
webhookQueue
}
from
"../../queues/webhook.queue"
;
import
{
webhookQueue
}
from
"../../queues/webhook.queue"
;
...
...
src/modules/webhooks/webhook.validation.ts
View file @
1bf2ac6c
import
{
z
}
from
"zod"
;
import
{
z
}
from
"zod"
;
import
{
WEBHOOK_DELIVERY_STATUS
,
WEBHOOK_EVENT
,
}
from
"../../common/constants/webhook.constant"
;
export
const
createWebhookConfigSchema
=
z
.
object
({
export
const
createWebhookConfigSchema
=
z
.
object
({
url
:
z
url
:
z
...
@@ -13,7 +17,7 @@ export const createWebhookConfigSchema = z.object({
...
@@ -13,7 +17,7 @@ export const createWebhookConfigSchema = z.object({
.
min
(
16
,
"Signing secret must be at least 16 characters long for security"
)
.
min
(
16
,
"Signing secret must be at least 16 characters long for security"
)
.
max
(
128
,
"Signing secret is too long"
),
.
max
(
128
,
"Signing secret is too long"
),
events
:
z
events
:
z
.
array
(
z
.
enum
([
"job.completed"
,
"job.failed"
]
))
.
array
(
z
.
nativeEnum
(
WEBHOOK_EVENT
))
.
min
(
1
,
"At least one event must be selected for notifications"
),
.
min
(
1
,
"At least one event must be selected for notifications"
),
});
});
...
@@ -25,7 +29,7 @@ export const updateWebhookConfigSchema = z.object({
...
@@ -25,7 +29,7 @@ export const updateWebhookConfigSchema = z.object({
.
max
(
128
,
"Signing secret is too long"
)
.
max
(
128
,
"Signing secret is too long"
)
.
optional
(),
.
optional
(),
events
:
z
events
:
z
.
array
(
z
.
enum
([
"job.completed"
,
"job.failed"
]
))
.
array
(
z
.
nativeEnum
(
WEBHOOK_EVENT
))
.
min
(
1
,
"At least one event must be selected for notifications"
)
.
min
(
1
,
"At least one event must be selected for notifications"
)
.
optional
(),
.
optional
(),
isActive
:
z
.
boolean
().
optional
(),
isActive
:
z
.
boolean
().
optional
(),
...
@@ -33,7 +37,7 @@ export const updateWebhookConfigSchema = z.object({
...
@@ -33,7 +37,7 @@ export const updateWebhookConfigSchema = z.object({
export
const
listWebhookDeliveriesQuerySchema
=
z
.
object
({
export
const
listWebhookDeliveriesQuerySchema
=
z
.
object
({
jobId
:
z
.
string
().
uuid
().
optional
(),
jobId
:
z
.
string
().
uuid
().
optional
(),
status
:
z
.
enum
([
"PENDING"
,
"SUCCESS"
,
"FAILED"
]
).
optional
(),
status
:
z
.
nativeEnum
(
WEBHOOK_DELIVERY_STATUS
).
optional
(),
page
:
z
.
coerce
.
number
().
int
().
min
(
1
).
optional
(
),
page
:
z
.
coerce
.
number
().
int
().
min
(
1
).
default
(
1
),
limit
:
z
.
coerce
.
number
().
int
().
min
(
1
).
max
(
100
).
optional
(
),
limit
:
z
.
coerce
.
number
().
int
().
min
(
1
).
max
(
100
).
default
(
20
),
});
});
src/queues/crawl.worker.processor.ts
View file @
1bf2ac6c
...
@@ -269,10 +269,6 @@ export async function processCrawlJob(job: Job): Promise<void> {
...
@@ -269,10 +269,6 @@ export async function processCrawlJob(job: Job): Promise<void> {
}
}
}
}
await
getJobRepository
().
updateStatus
(
jobId
,
JOB_STATUS
.
RUNNING
,
{
startedAt
:
new
Date
(),
});
try
{
try
{
if
(
crawlJob
.
mode
===
CRAWL_MODE
.
SCRAPE
)
{
if
(
crawlJob
.
mode
===
CRAWL_MODE
.
SCRAPE
)
{
console
.
log
(
console
.
log
(
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment