mirror of
https://github.com/ever-co/ever-gauzy.git
synced 2026-10-02 01:54:50 +08:00
develop
27951
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3f45758e76 |
Merge pull request #10250 from ever-co/fix/media-plugin-employee-scope
fix(security): scope video, camshot and soundshot reads to the caller's own recordingsv111.44.13 |
||
|
|
c0b41d4c34 |
Merge pull request #10247 from ever-co/fix/screenshot-delete-ownership
fix(security): restrict screenshot deletion to the caller's own screenshotsv111.44.12 |
||
|
|
fa5ede68c9 |
Merge pull request #10246 from ever-co/fix/weekly-report-500
fix(core): stop reports 500ing when the request names no time zonev111.44.11 |
||
|
|
5453e2de2d |
Merge pull request #10252 from ever-co/fix/sensitive-relations-tracked-data
fix(security): gate tracked data reached through client-supplied relations |
||
|
|
f76472617e |
Merge pull request #10251 from ever-co/fix/custom-tracking-employee-scope
fix(security): scope custom-tracking sessions to the employees the caller may see |
||
|
|
019b369bde |
Merge pull request #10249 from ever-co/fix/null-employee-identity
fix(security): treat a missing employee identity as no access, not no filter |
||
|
|
59b3dba7af |
fix(security): stop exempting DEMO from the signing-secret rules (GHSA-39j7) (#10253)
The first pass left `DEMO=true` on the historical path — an unset secret fell back to the literal published in this repository — because it could not be verified whether demo.gauzy.co relied on that fallback, and develop deploys straight there. It does not. Every deployment in the fleet sets all four secrets explicitly (JWT_SECRET, JWT_REFRESH_TOKEN_SECRET, JWT_VERIFICATION_TOKEN_SECRET, EXPRESS_SESSION_SECRET are 64-96 bytes in ever-gauzy dev/stage/prod and ever-teams dev/stage/prod), so the exemption protected nothing here while leaving any other DEMO deployment signing tokens with a key anyone can read in this repository — the hole the advisory describes. DEMO now behaves like every other environment: an explicitly set secret is used as-is, an unset one gets the per-process random value, and the startup guard still reports that as unconfigured. `resolveSecret()` loses its second argument, which only existed to carry the literal it was allowed to return. The spec's DEMO block now asserts the opposite of what it used to, with a control arm that shows a token forged with the published key verifying against the old fallback and failing against the new value. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>v111.44.10 |
||
|
|
b781271668 |
Merge pull request #10236 from joel-kalema/feat/polish-goals-dialogs
Feat/polish goals dialogsv111.44.9 |
||
|
|
23683add1b |
Merge pull request #10248 from ever-co/fix/candidate-billing-rate-decimals
fix(employee,candidate): cap billing rates, carry minimum rate on hire, honor transformers under MikroORMv111.44.8 |
||
|
|
3e1a8810b1 |
fix(security): scope video, camshot and soundshot reads to the caller's own recordings
The three media plugins let a caller read another employee's recordings, with only TIME_TRACKER —
a default EMPLOYEE permission.
- The list handlers pinned `uploadedById` to the caller and then spread the client's `where` OVER it
(`where: { ...where, ...params.where }`), so `?where[uploadedById]=<someone else>` won. The server's
keys now go last, which also stops a client-supplied `tenantId`/`organizationId` from replacing the
resolved ones.
- The by-id handlers were tenant-scoped only. These entities carry `uploadedById`, not `employeeId`,
so the per-employee restriction in TenantAwareCrudService never applied to them. They now go through
`assertCallerOwnsUpload` in core, shared by all three plugins rather than copied into each: it
answers 404 for a record the caller did not upload, so the id of someone else's recording stays
unconfirmed, and leaves CHANGE_SELECTED_EMPLOYEE holders alone.
Specs: the ownership rule in core, and per plugin the merge order, the tenant/organization keys and
the delegation. Camshot's tsconfig.lib.json now excludes spec files, as the videos and soundshot ones
already did.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
c3ab58e738 |
fix(employee,candidate): address review — cap rates at the old integer limit, support transformer arrays
- BILLING_RATE_MAX is now 2147483647, the old integer column's maximum, so every accepted rate stays revertible: rolling the columns back to integer cannot overflow on a value saved after the upgrade (Greptile P1). numeric(14,2) stays wider; the cap can be raised without a migration. - ValueTransformerType supports TypeORM's ValueTransformer[]: `to` in array order, `from` reversed, matching ApplyValueTransformers. Calling .to()/.from() on the array threw before (CodeRabbit). - The wrapper and the property now report the same DDL, including an explicit columnType, and declaredColumnType keeps length and precision-only modifiers (CodeRabbit). - Real MikroORM round trip on better-sqlite: generated DDL, write conversion, hydration and null handling for a numeric and an int-backed enum column (Greptile P2). Verified by mutation: it fails without the bridge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f915b69e10 |
fix(security): restrict screenshot deletion to the caller's own screenshots
`deleteScreenshot` limits a caller without CHANGE_SELECTED_EMPLOYEE to their own screenshots by joining the owning time slot. The TypeORM branch used a LEFT join, which keeps the row when its ON clause does not match, so the ownership condition removed nothing: the effective filter was id + tenant + organization. DELETE_SCREENSHOTS is a default EMPLOYEE permission, so any member of an organization could delete a colleague's screenshot, and with `forceDelete` the stored image as well. Ids come from any of the list endpoints. - INNER join instead, so a screenshot whose slot belongs to someone else cannot match. - A caller with no employee identity and no CHANGE_SELECTED_EMPLOYEE now gets 403 instead of having the ownership predicate quietly dropped; the same guard is applied to the MikroORM branch, where a null id would otherwise have searched for slots with a NULL employeeId. - Spec covers all three cases; two of them fail on the previous code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
685950bd4b |
fix(core): let a deliberate HTTP answer through the team statistic handler
`GetOrganizationTeamStatisticHandler` flattened every error into
`BadRequestException('Failed to execute organization team statistic query')`. The sensitive-relation
check answers 403 from inside that call, so a caller probing
`GET /organization-team/:id?relations[]=members.employee.timeSlots.screenshots` was told the request
was malformed rather than forbidden — the data was blocked either way, but the reason was masked, and
so was every other deliberate HTTP answer from below.
An `HttpException` now passes through unchanged; anything else still becomes the generic 400.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
934471957e |
fix(security): gate tracked data reached through client-supplied relations
The per-employee restriction in TenantAwareCrudService applies to the ROOT entity of a read, so a
client-supplied `relations` that starts from a row the whole organization may read walks straight
into everybody's tracked data. Both of these are reachable with default EMPLOYEE permissions
(ORG_TASK_VIEW, ORG_TEAM_VIEW) and were reproduced against a booted API:
GET /tasks/:id?relations[]=timeLogs.timeSlots.screenshots
GET /organization-team/:id?relations[]=members.employee.timeSlots.screenshots
- `TRACKED_DATA_SENSITIVE_RELATIONS` names the hops that cross from a shared entity (Employee, Task,
OrganizationProject, OrganizationTeam) into tracked data, and requires CHANGE_SELECTED_EMPLOYEE for
them. The existing walk already gates by the entity a relation is loaded from, so it needed only to
arm this table per hop, merged with whatever the organization table declared — `employees.user`
keeps needing ORG_USERS_VIEW.
- `TimeLog.timeSlots` and `TimeSlot.screenshots` are deliberately NOT gated: those reads start from a
row already scoped to the caller's own employee id (the desktop retry queue, the screenshot modal),
and reaching someone else's slot needs one of the hops above first.
- `GET /timesheet/timer/status/worked` may legitimately name a teammate, so the table cannot help
there: it narrows the requested relations instead, dropping the tracked-data ones for a caller
without CHANGE_SELECTED_EMPLOYEE. Narrowing rather than refusing keeps existing clients working.
Specs cover the three exploited paths, the admin bypass, the self-service reads that must keep
working, a same-named relation on an unrelated entity, and the narrowing helper.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
bf33e5bdf4 |
fix(security): scope custom-tracking sessions to the employees the caller may see
`getTrackingSessions` took the `employeeIds` of the request as given, with no permission and no manager check, and the controller gate is OR-semantics over TIME_TRACKER / ALL_ORG_EDIT / ALL_ORG_VIEW. Every default EMPLOYEE holds TIME_TRACKER, so `GET /timesheet/custom-tracking/sessions?employeeIds[]=<colleague>&includeDecodedData=true` returned that colleague's sessions and their decoded payloads. Reproduced against a booted API. The ids now go through `ManagedEmployeeService.filterAccessibleEmployeeIds`, the same gate the time-log and statistics reads use, with the request's team and project scope passed along so a manager keeps their reach. CustomTrackingModule imports EmployeeModule for it, the way TimeLogModule already does. The `#10212` guard on these routes does not help here: it enforces an organization setting, it does not scope by employee. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1e0fe90447 |
fix(security): Inherited mutating routes + authz residuals (GHSA-v79w, GHSA-c3cj, GHSA-44pv) (#10242)
* fix(security): gate inherited mutating routes and close authorization residuals GHSA-v79w-54p2-wmh5 (high): CrudController's inherited POST/PUT/DELETE/soft/ recover routes carry no permission metadata, and PermissionGuard passes when the metadata is empty, so an EMPLOYEE could soft-delete users, contacts and tags. Those routes are now gated, and a repo-wide spec walks every controller in core and the plugins (966 mutating routes) and fails on any ungated one unless it is explicitly opted in: 34 deliberately open routes, each with the service-side check that covers it, plus a frozen ratchet list of pre-existing bare routes that may only shrink. crud.service softRecover also passed no withDeleted, so every inherited recover route answered 404 for the row it exists to restore. GHSA-c3cj-m3xm-7j5h (high): the organization-contact lookup built its joins straight from client-supplied relations, bypassing the sensitive-relations interceptor. It now asserts permissions and resolves through an allow-list, and pins the employee to the caller unless they may change the selected employee. GHSA-44pv-34gx-q9p4 (medium) residuals: validators that fell open on an undefined key now fail closed, a DTO carrying both organization and organizationId validates both instead of neither, and email-template listings and writes are pinned to the caller's tenant plus the global templates. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(cspell): add the new vocabulary and use US spellings Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(security): close the review findings on the authz residuals Review pass over PR #10242 (CodeRabbit, Greptile, SonarCloud). Two of the findings were real gaps in the fixes this PR exists for. @Roles() is not a gate on its own (CodeRabbit + Greptile, major) The repo-wide mutating-route scan accepted any @Roles() decorator as authorization. @Roles() only calls SetMetadata, and RoleGuard is NOT registered as an APP_GUARD (app.module.ts provides only the throttler), so a handler declaring roles without @UseGuards(RoleGuard) reads as role-gated while nothing ever consults those roles — exactly the kind of ungated mutating route the scan exists to catch. The scan now requires an effective RoleGuard, on the handler or on its controller, before it counts @Roles() as a gate; @Public() is unchanged. Every @Roles() in the tree today is already paired with @UseGuards(RoleGuard), so the ratchet lists are unchanged, and a new in-memory test pins both shapes. An organization named as a bare id string skipped the employee check (CodeRabbit, major) EmployeeBelongsToOrganizationConstraint resolved the organization by reading `organization.id` only. A DTO that does not extend TenantOrganizationBaseDTO has no @IsObject() to refuse a string — e.g. EmployeeRecurringExpenseQueryDTO, which intersects EmployeeFeatureDTO alone, behind GET /employee-recurring-expense/month — so `?organization=<uuid>&employeeId=<foreign>` left the id unresolved and fell through to the deliberately permissive "no organization named" branch. Both shapes now resolve to the same value, so the membership lookup runs with the id the request actually named. MikroORM findAll could read NULL-tenant rows of an organization (CodeRabbit) `tenantId: mTenantId ?? null` kept the tenant arm alive without a tenant in context, and MikroORM compiles a literal null to IS NULL, so the arm matched every NULL-tenant row rather than nothing. The tenant arm is now built only when there is a tenant, which is what scopeEmailTemplateWhere already did for the pagination route. CreateEmailTemplateDTO declared only organizationId (CodeRabbit) name, languageCode and hbs are NOT NULL on email_template, so a create missing one could never persist; declaring them turns a database error into a 400 and lets Swagger publish the real create schema. The route now validates with whitelist, so an undeclared body key cannot reach persistence at all — stripEmailTemplateScopeFields stays as the explicit statement of which fields are scope fields. Also the five new SonarCloud code smells: braces around the two switch cases in EmailTemplateService.findAll (lexical declarations in a case block), `IFindOneOptions<T> | unknown` collapsed to `unknown`, two useless `?? {}` spreads, and toHaveLength in the opt-in-list assertion. Refused: nothing. No finding asked for a protection to be weakened. Tests: packages/core src/lib/{shared/validators,shared/guards,core/crud, core/dto,email-template,organization-contact,employee-recurring-expense/dto} -> 15 suites, 212 tests, all passing (was 13/191 plus one non-compiling suite). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(test): null-check request.query consistently in the guard control arm DeepScan INSUFFICIENT_NULL_CHECK on organization-permission.guard.spec.ts:677 — the CONTROL arm replays the pre-fix `extractRequestOrganizationId` with optional chaining on `request.query`, then asserted on `request.query.organizationId` without it. The object is a local literal so nothing could have thrown, but the two readings of the same expression disagreed. Both now use `?.`, which keeps the replay a verbatim copy of the pre-fix production code. No behaviour change; the suite still passes (45 tests). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(email-template): validate languageCode against LanguagesEnum Second CodeRabbit pass, on the DTO this PR just added. - languageCode was `@IsString()`, so `xx` was accepted and persisted. Every reader of the column looks a template up by a LanguagesEnum value (the seeder, saveTemplate, the mailer) and the column has no enum constraint, so such a row is one nothing can ever find. Now `@IsEnum(LanguagesEnum)`, with a test for a rejected code and an accepted non-default one. - The whitelist regression test passed vacuously: the fixture carried no tenant keys, so it would have stayed green even if the DTO later declared them. It now puts `tenant` and `tenantId` in the input, asserts they are on the transformed instance, runs the same whitelisting `validate()` the pipe runs, and asserts only the four template fields survive. packages/core src/lib/email-template/dto -> 1 suite, 8 tests, passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(email-template): answer create with the stored row, not the request body CodeQL flagged the create route as reflected XSS: it returned the body it had just persisted. Not exploitable — the response is JSON and helmet sets X-Content-Type-Options: nosniff — but echoing the request buys nothing. The route now reads the row back through the tenant-scoped lookup, so the client sees the scope fields the server pinned rather than the ones it sent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>v111.44.7 |
||
|
|
2bc0b39cd0 |
fix(security): Nested-relation ownership checks (GHSA-jh6m, GHSA-gwpq, GHSA-6qvm) (#10238)
* fix(security): check ownership of nested relations, not just the root row GHSA-jh6m-9fxr-rx3c (high): TenantAwareCrudService verified the tenant of the row being written but not of the rows named inside it, so PUT /invoices/:id carrying invoiceItems with another tenant's item id re-parented or overwrote that row, and PUT /candidate/:id could cascade into another user's credentials. A new assertGraphNotForeign() walks the payload's relation graph (depth-bounded, one batched SELECT per relation), refuses foreign-tenant rows through any relation, refuses re-parenting a global NULL-tenant row, stamps the caller's tenant on cascaded inserts, and strips credential fields from a nested existing User. Candidate updates no longer cascade into User at all. Also closes the named siblings: request-approval's raw employee and team lookups are tenant-scoped (GHSA-gwpq-mmw7-vx85); UpdateTimeSlotHandler, the bulk activity save and the time-log routes are tenant-scoped, whitelisted and no longer accept a foreign employeeId, and the organization policy guard checks the target's tenant for SUPER_ADMIN too (GHSA-6qvm-3wg4-26w4). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(cspell): add the new vocabulary and use US spellings Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(security): link nested users instead of writing them, fail closed on the report queries Review follow-up on the nested-relation ownership work. Six findings held up when checked against the code; each protective line has a spec arm that fails when the line is reverted. - nested-graph: an EXISTING `User` reached through a cascade is now reduced to `{ id }`. Removing only the credential columns still let `user: { id, firstName }` rename any account of the caller's own tenant, which a tenant check cannot catch — the victim is a legitimate member of that tenant. A NEW user inserted through a cascade (candidate sign-up) is untouched. CandidateCreateHandler re-attaches the user it just created to its response, so POST /candidate still answers with the user the UI prints. - nested-graph: a payload that would still persist rows BELOW the walk limit is now refused (400) instead of being waved through. The row sitting at the limit was checked, but its own relations were not walked, and `save()` would still cascade or re-parent them with no ownership check at all. - user create: `UserCreateCommand` drops a body-supplied `id`. `CrudService.create()` upserts when the payload carries a primary key, so a body id turned "create a user" into "overwrite that user" — and POST /candidate / POST /employee hash the request's `password` into that payload, so `user: { id: <an admin of my tenant> }` reset that account's password and demoted its role. Same class as the candidate update cascade this branch already closed, through the create route instead. - time logs / time slots: `getTimeLogs()`, the report queries built on `getFilterTimeLogQuery()` / `buildMikroOrmTimeLogWhere()` and `getTimeSlots()` build their own queries, so the never-matching employee condition added to the CRUD reads never applied to them. Their employee predicate is only added when `employeeIds` is non-empty, and `employeeIds` is only narrowed for a caller who HAS an employee record — so a caller with neither CHANGE_SELECTED_EMPLOYEE nor an employee read the whole organization, or the employees they named in the body. They now match nothing, exactly as `findConditionsWithoutOwnEmployee` already does. - activities: `scopeActivitiesForWrite()` now drops a `projectId` / `taskId` (and folds a `project` / `task` object into its id) that does not name a row of the caller's tenant. These are plain foreign keys, so the nested-graph check never sees them, and the activity is written through the raw repository: a foreign projectId was stored verbatim and handed back with the victim tenant's project joined onto it. The bulk handler now applies its request-level `projectId` before that check rather than over it. - OrganizationPermissionGuard: the SUPER_ADMIN exemption resolves the tenant before the no-target shortcut. Not exploitable today (TenantBaseGuard / TenantPermissionGuard already refuse a tenant-less request on both controllers that apply this guard), but the guard no longer depends on a sibling guard for it. Also, for the two new SonarCloud complexity findings: the reference rules of `assertGraphNotForeign` move into `resolveReference()` / `isGlobalRowWritable()`, and `UpdateTimeSlotHandler.execute` into `resolveEmployeeScope()` / `collectChanges()` / `saveActivities()`. Behaviour is unchanged; the existing specs cover both. Two review findings were NOT applied, deliberately: - scoping the request-approval employee / team lookups by `RequestContext.currentOrganizationId()`: cross-organization references inside one tenant are a different boundary (the advisories are about cross-tenant writes), and the header-derived organization is absent on legitimate calls, which would silently drop approvers. - nothing was relaxed to silence a finding; no test was deleted or loosened. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(time-tracking): drop the redundant optional chaining, keep a null employeeId meaning "no scope" Follow-up on the review fixes, for DeepScan's three new findings on the previous commit (all in the code it added). - `user` is dereferenced unguarded a few lines above each of the new fail-closed guards, so `user?.employeeId` in them was a null check that can never fire; it now reads `user.employeeId` like the surrounding code (time-log report filters, both ORM branches, and `getTimeSlots`). - `UpdateTimeSlotHandler.resolveEmployeeScope()` returned `ID | undefined | null`, where `null` meant "refuse" — but a caller holding CHANGE_SELECTED_EMPLOYEE may legitimately send `employeeId: null` (no employee filter), and that would have been read as a refusal. It now returns a scope object or `null`, so the two cases cannot be confused. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(time-tracking): take the organization from the verified employee, not from the body Two more review findings, both real: the manual-time routes and the bulk activity save validated the employee against the caller's tenant but then persisted whatever `organizationId` the body carried. - `addManualTime` / `updateManualTime` already read the `futureDateAllowed` policy from `employee.organization`, so a body organizationId of the same tenant had the write judged by one organization's rules and then filed — time log, time slots and timesheet — under another's. On update it also decided which rows counted as conflicting, i.e. which sibling organization's time slots this call was allowed to delete. Both now derive the organization from the employee, with the body value left as a fallback for an employee that has none. - `BulkActivitiesSaveHandler` used the employee's organization only when the body omitted one. An Employee row belongs to exactly one organization, so the body value could only ever file that employee's activities under an organization they are not a member of; the employee's own organization now wins. Each is covered by a spec arm that fails when the line is reverted (verified by revert-and-run, files restored and hashed). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(core): split the per-relation walk and the activity normalisation out SonarCloud's two remaining cognitive-complexity findings on this branch, both in code it added. Pure extraction, no behaviour change: - `assertGraphNotForeign()` keeps the level-by-level loop; the per-relation lookup and the per-reference rules move into `walkRelation()` (31 -> well under the threshold). - `scopeActivitiesForWrite()` hands the relation-object stripping and the `project` / `task` fold to `normalizeReferences()`. The nested-graph and activity specs (112 tests, real better-sqlite3 databases with their CONTROL arms) cover both and stay green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(user): align the create handler with the merged role normalization Merging develop brought in GHSA-x4mv's role handling: assertCanAssignRoles now takes the payload and extracts every role form itself, and a role/roleId pair naming two different roles is a 400. This spec still asserted the older two-argument contract with a contradicting pair, so the two changes were green apart and red together. It now covers both: the agreeing pair is validated, and the contradicting pair is refused before anything is persisted. Also replaces a stray console.log with the Nest logger. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3a62724c23 |
fix(security): Social-login audience verification + purpose-bound tokens (GHSA-58x4, GHSA-28wv) (#10241)
* fix(security): verify social-login token audience and bind every purpose token
GHSA-58x4-7mw9-gmqg (critical): POST /auth/signin.email.social accepted any
provider access token that resolved to a victim's email — another app's token or
a GitHub PAT — and signed the caller in as that user. Each provider is now
introspected against an allow-list of OAuth client ids (Google tokeninfo aud/azp
plus email_verified, GitHub /applications/{client_id}/token, Facebook
debug_token app_id) and fails closed when no client is configured. Twitter/X is
refused, since it exposes no verified email. One normaliser rejects an empty id
or email, so an undefined value can no longer reach a find() and be dropped by
TypeORM's undefined:'ignore' behaviour, which returned every user in every
tenant.
GHSA-28wv-vrxj-rp4q (medium): tokens signed with JWT_SECRET were interchangeable.
New signPurposeToken/verifyPurposeToken pin a purpose claim, required non-empty
claims and HS256. Workspace sign-in, invoice share, estimate, invite, team-join,
appointment and password-reset tokens are typed; public invoice and estimate
links are bound to the stored row and the URL id; access-token consumers
(JwtStrategy, RegisterAuthorizationGuard, Zapier, Plane) reject a token whose
purpose says it is something else. Untyped legacy tokens are accepted only where
they are also bound to a stored row, and never on signin.workspace.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(cspell): add the new vocabulary and use US spellings
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(auth): review follow-ups on the purpose-token hardening
Addresses the bot review round on PR #10241. No security behaviour is relaxed;
every change either tightens a check or is a readability fix with the same
runtime semantics, and the affected suites were re-run (9 suites / 155 tests).
- Password reset now goes through `verifyPurposeToken(TokenPurposeEnum.PASSWORD_RESET)`
instead of a bare `verify()` plus a string-literal purpose comparison
(CodeRabbit). It additionally requires a non-empty `id` claim: an `undefined`
id reaching `findOneByIdString` widens the lookup instead of failing closed,
which is exactly the class of bug this PR exists to remove. The stored
password_reset row still binds the token, so this is defence in depth.
`verify` and `JWT_ALGORITHMS` are no longer imported there.
- `JwtStrategy.validate` moves the employeeId/organizationId claim checks into
`attachEmployeeAndOrganizationContext` (SonarCloud: cognitive complexity 16 >
15). The helper RETURNS the UnauthorizedException instead of throwing, so the
exact exceptions and messages the callback received before are unchanged, and
three specs now cover the organization branch (member missing, employee in
another organization, happy path). Control: inverting the rejection makes 12
of the 30 tests in that suite fail.
- `normalizeSocialIdentity` extracts its nested ternary into
`normalizeProviderAccountId` (SonarCloud), same accepted values as before:
trimmed string, or a positive safe integer stringified for GitHub.
- `Number.NaN` over `NaN` in the reschedule-token lifetime (SonarCloud), and the
two unused `catch (error)` bindings in PublicInvoiceService are now bare
`catch`.
Not changed, deliberately: Greptile's P1 "mixed-case emails fail lookup". The
social lookup already queries BOTH the normalised (lowercased) address and the
provider's exact spelling, which is a strict superset of what this code did
before the PR, so nothing regressed. Matching stored emails case-insensitively
would let the holder of `a@x.com` sign into an account stored as `A@x.com` —
a widening of an authentication lookup that password login does not perform —
and belongs in a repo-wide email-normalisation change, not in this advisory fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
v111.44.6
|
||
|
|
7a4b82b621 |
fix(security): treat a missing employee identity as no access, not no filter
The tracked-data queries apply their employee predicate only when the id list is non-empty (`if (isNotEmpty(employeeIds))`). `ManagedEmployeeService.filterAccessibleEmployeeIds` returned `[]` for a caller with no employee identity, and the counts queries scope by hand with `if (user.employeeId && ...)`, so such a caller got no predicate at all and read the whole organization's tracked data: time slots with their screenshots, application and window titles, manual times, members and per-project totals. The token is self-serve. `POST /auth/switch-organization` needs CHANGE_SELECTED_ORGANIZATION, a default EMPLOYEE permission; switching to an organization where the user has a user_organization row but no employee record mints a token with `employeeId: null`, and `IsOrganizationBelongsToUser` then still accepts the caller's own organization in the query. A user whose employee record was soft-removed logs in the same way, since the User row stays active. - `filterAccessibleEmployeeIds` and the hand-rolled scoping in `statistic.service.ts` now return `NO_ACCESSIBLE_EMPLOYEE_ID` (the nil UUID) for an authenticated caller with no employee identity: the predicate stays in place and matches nothing, so the answer is empty instead of everything. - An organization-wide viewer (`ALL_ORG_VIEW`) keeps the access their role gives them. - A request with no user at all — a public share link, an internal call — is left to the scoping its own caller applies, so `public-share` team pages are unaffected. - The three hand-rolled sites now share one helper, which also removes the duplicated block. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
44321d6e8e | Merge remote-tracking branch 'origin/develop' into fix/candidate-billing-rate-decimals | ||
|
|
7affd5ab71 |
fix(employee,candidate): cap rates, carry the minimum rate on hire, honor transformers under MikroORM
Follow-ups from review of this PR: - @Max(999999999999.99) on all four rate fields, so a value the numeric(14,2) column cannot hold is a clean 400 from the DTO instead of a database overflow, and cannot block a rollback to the old integer column. - CandidateHiredHandler now copies minimumBillingRate into the new employee. It only copied billRateValue, so a candidate's minimum rate was silently dropped on hire (bug, pre-existing). - MikroORM ignored the TypeORM `transformer` column option: @MultiORMColumn handed it to @Property, which drops it, so with DB_ORM=mikro-orm money columns skipped rounding and validation and int-backed enum columns (actor type, availability status) were stored and read raw. parseMikroOrmColumnOptions now wraps the transformer in a MikroORM type and keeps the declared column DDL (numeric(14,2)). - Specs for each, including the hire handler (new) and the transformer bridge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c514654389 |
fix(core): stop reports 500ing when the request names no time zone
`moment().tz(undefined)` returns undefined instead of a moment, so every report that groups its rows by `.tz(timeZone).format(...)` answered 500 "Cannot read properties of undefined (reading 'format')" as soon as the organization had rows to group. `GET /timesheet/time-log/report/weekly` was found this way while booting the API during the #10212 review; it fails for SUPER_ADMIN too. - Add `resolveTimeZone()` next to `getDaysBetweenDates` and use it there, so the day list and the grouping keys of a report always come from one and the same zone. The fallback is the server zone, which is what `getDaysBetweenDates` and the group-by command handlers already defaulted to. - Apply it to the five report bodies in `time-log.service.ts` (weekly, daily charts, daily, owed amount and its charts, time limit) and to `payment.service.ts`. - Regression specs: every report method answers with no time zone and with an empty one, the rows land in a bucket the day list actually contains, and a named zone is still honoured. Without the fix these reproduce the exact production TypeError. `RecordingQueryBuilder` grew a `rows` field, because the report bodies only run over a non-empty result — which is why the existing filter specs never caught this. Measured before and after against a booted API on a throwaway database: five routes (weekly, daily-chart, owed-report, owed-charts, time-limit) went from 500 to 200 once the organization had a time log; nothing else changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b45fc7ee85 |
fix(security): MJML file-include LFI + integration-setting takeover (GHSA-48h9, GHSA-4rwq) (#10239)
* fix(security): compile MJML without file includes, lock down integration settings GHSA-48h9-vwf5-h8m7 (critical): an email or accounting template containing mj-include with a path was compiled with the file loader enabled, so any caller who could preview or save a template could read files from the API container, including .env and /proc/self/environ. All nine mjml2html call sites now go through compileMjml(), which passes ignoreIncludes: true (verified against mjml-parser-xml 4.18.0, which returns before any read) and coerces the source to a string, so a JSON body can no longer arrive as a pre-parsed Handlebars AST. An ESLint rule keeps raw mjml imports out of the package, and the preview routes now validate their bodies. GHSA-4rwq-65wh-45h4 (high): PUT /integration-setting/:id could rewrite server-managed settings such as a GitHub installation_id, binding another tenant's installation. Updates are now tenant-scoped, restricted to an allow-list of user-editable keys, and pin settingsName and integrationId; the inherited POST /integration-tenant route that reached the same sink is gated; and installation_id is canonicalised so the uniqueness check compares like with like. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(cspell): add the new vocabulary and use US spellings Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(security): harden template source coercion, address review findings Review follow-ups on the GHSA-48h9 / GHSA-4rwq fix. No security boundary is relaxed; the coercion helper gets strictly stronger. - toTemplateSource(): map every non-primitive (a JSON object or array, a Handlebars AST, a function) to '' instead of stringifying it. `String(value)` produced a useless '[object Object]' and, for an object whose `toString` and `valueOf` are not callable ({"toString":1,"valueOf":2} is valid JSON), threw a TypeError out of the request handler instead of rendering an empty template. The preview DTOs already reject a non-string `data`, so this is the second layer, and it is the layer the stored-`hbs` render path relies on. Primitives still stringify. Spec updated to assert the stronger outcome (the AST now renders to '', not to '[object Object]') and extended with the throwing shape. Also clears SonarCloud "'source ?? ''' will use Object's default stringification format". - IntegrationTenantController.create(): replace the nested ternary that encoded "not an array means refuse" as a sentinel `[null]` element with an explicit guard. Same three outcomes (absent settings allowed, array checked element-wise, anything else forbidden), one fewer indirection. Clears SonarCloud "Extract this nested ternary operation". - GITHUB_INSTALLATION_ID_PATTERN: `\d` for `[0-9]`. Identical in JS (`\d` is ASCII-only, with or without the `u` flag); clears a SonarCloud nitpick. - integration-setting.utils.ts: record WHY the allowlist lookup stays on `Object.prototype.hasOwnProperty.call`. SonarCloud asks for `Object.hasOwn`, but that is ES2022 and packages/core/tsconfig.lib.json targets es2021 with no `lib` override, so it fails to compile (TS2550, verified with tsc). Tests: packages/core email-template + accounting-template + integration-setting + integration-tenant = 8 suites, 123 tests passed; integration-github = 2 suites, 48 tests passed. ESLint over the changed core files: 0 errors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>v111.44.5 |
||
|
|
60eacd8a92 |
fix(security): Tenant-scoped invoice numbering (GHSA-57hw, GHSA-w3mx) (#10244)
* fix(security): scope the invoice number sequence to the tenant GHSA-57hw-jqpj-ww97 (medium): GET /invoices/highest ran an unscoped MAX over every tenant's invoices, so any authenticated user learned the highest invoice number in the installation, and numbering leaked business volume across tenants. The aggregate is now tenant-scoped in both ORM branches and fails closed without a tenant. Because scoping alone would collide with numbers other tenants already hold, the global unique on invoiceNumber becomes unique per (tenantId, invoiceNumber), with a per-dialect migration for postgres, mysql and sqlite. GHSA-w3mx-m5cr-3gxp residual (low): a tenant AI-provider credential with no baseUrl still reached the provider's built-in loopback default. Every tenant-sourced credential is now treated as tenant-controlled, so it is blocked unless private base URLs are explicitly allowed; only an operator-set environment base URL bypasses the flag. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(cspell): add the new vocabulary and use US spellings Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(invoice): make the MySQL uniqueness migration resumable and reject fractional numbers Review follow-up on the tenant-scoped invoice numbering change. MySQL commits every DDL statement on its own, so `migrationsTransactionMode: 'each'` does not roll the CREATE and the DROP of `mysqlUpQueryRunner` back together. If the process died between them — or the information_schema lookup failed — the new composite index survived while the migration row was never written, and the retry on the next boot died on `Duplicate key name`, taking the API down until someone repaired the schema by hand. Both MySQL branches now look the index up by its COLUMNS first (`GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX)`, so column order is part of the match and the primary key never matches), create only what is missing and drop every same-shaped leftover whatever it is named. That also fixes the reverse case the reviewers pointed out: `down()` used to drop only the index name this migration happens to use. `invoiceNumber` now requires a whole number. `numeric` (Postgres/SQLite) stores a fraction that MySQL's `bigint` truncates, so a fractional number made `MAX(invoiceNumber) + 1` mean different things per database; `@IsNumber()` accepted it. `@IsInt()` already implies a number, so it replaces rather than joins `@IsNumber()`. `up()`/`down()` dispatch through a table instead of the two switch statements every migration in this folder repeats verbatim. The behaviour is identical (including the `Unsupported database` throw); it is here because those ~40 boilerplate lines were counted as duplicated new code and failed the Sonar quality gate for this PR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(invoice): cover the MySQL branch of the uniqueness migration The MySQL resumability fix was argued from the MySQL manual, not exercised: no MySQL server is available in this environment. It does not need one — the migration only ever learns which unique indexes exist through `information_schema.STATISTICS`, so a stub query runner over a simulated index set drives the real branch end to end and records the DDL it issues. Five cases, including the two the review raised: a crash between the CREATE and the DROP (the index is found, no second CREATE is attempted, the old one is still dropped), a completed run re-entered (no statements at all), an index the migration chain did not name, and a revert that has to drop a composite index under an unexpected name. The primary key shares the `NON_UNIQUE = 0` filter and is asserted to survive all of them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>v111.44.4 |
||
|
|
bf5aea9a7b |
fix(security): Role-reference normalization + credential leak (GHSA-x4mv, GHSA-hh83, GHSA-hjcg) (#10245)
* fix(security): resolve every form of a role reference before authorizing it GHSA-x4mv-fhwj-g3rp (high): the role-change check read entity.role?.id and entity.roleId, but TypeORM also accepts a relation given as a bare id string, so a role sent as a plain SUPER_ADMIN uuid was invisible to the check and still persisted — self-escalation through the profile update. extractRoleIds and normalizeRolePayload now read every representation, reject a present but unresolvable role, refuse a role/roleId pair naming two different roles, and normalise the payload so the value that was checked is the value that is saved. Wired into updateProfile, UserCreateHandler, the register handler, invites (which previously stored a role the check never saw), employee and candidate creation, and the DTO validator. GHSA-hh83-hq74-gh9f (low): under DB_ORM=mikro-orm, wrap(entity).toJSON() ignores class-transformer, so a populated User in a listing carried its credential columns. refreshToken, code and codeExpireAt are hidden at the ORM level, and a last-line scrub removes credential keys from User-shaped objects in responses. GHSA-hjcg-633x-qq74 hardening: the register guard tenant-checks every role identifier in the body instead of only the first one it finds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(cspell): add the new vocabulary and use US spellings Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(invite): validate the role payload before branching on the inviter `createBulk` only read the body's role references inside the fallback branch, so the checks rode on the inviter's own role: an EMPLOYEE inviter, who is force-assigned the EMPLOYEE role, had a malformed or self-contradicting payload (`roleId` and `role` naming two different roles, `role: {}`, `roleId: ''`) silently accepted, while every other inviter got a 400 for the same body. That was never an escalation — the EMPLOYEE branch persists the checked role, which is the point of GHSA-x4mv-fhwj-g3rp — but an answer that depends on who is asking is a bad place to keep input validation, and it leaves the one caller whose role is overridden as the only one whose payload nobody parses. The extraction now runs once, before the branch, and a body naming two different roles is refused for everyone. The fallback keeps its own "exactly one" rule, since an invitation there cannot be issued without a role. Regression coverage added for the EMPLOYEE inviter, plus a control that a body with no role at all still issues an EMPLOYEE invitation. Also splits `scrubNode()` out of `scrubUserCredentials()` so the walker stays under SonarCloud's cognitive-complexity threshold (17 -> 9). No behaviour change: the same nodes are visited and the same keys deleted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(cspell): use the US spelling in the new invite comment Cspell flagged "licence" in the comment added by the previous commit; the wording now avoids the word entirely. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>v111.44.3 |
||
|
|
ebf676b2f8 |
fix(security): No published default signing secrets (GHSA-39j7, GHSA-chm8, GHSA-4r2r, GHSA-3cgp) (#10240)
* fix(security): never sign tokens with a published default secret
GHSA-39j7-x845-4w3c (high) and GHSA-chm8-2ggf-pgjq: when JWT_SECRET,
JWT_REFRESH_TOKEN_SECRET, JWT_VERIFICATION_TOKEN_SECRET or
EXPRESS_SESSION_SECRET was unset, the API fell back to a literal published in
this repository, so anyone could forge tokens and sessions. resolveSecret() now
substitutes a per-process random value instead, and the startup guard still
reports such a secret as unset, so production keeps refusing to boot. The
known-default list is shared by the config resolver, the startup guard and the
desktop apps, and it catches a default published for any key, not only its own.
Desktop apps generated no secrets at all: every install shipped the same baked
DESKTOP_JWT_* values while binding the local API to 0.0.0.0. They now provision
random per-install secrets on first run and rotate a stored published default.
DEMO=true keeps today's behaviour, with a TODO, pending a decision on
demo.gauzy.co's secrets.
Also: the MCP refresh grant re-checks the account on every refresh instead of
trusting a 30-day-old token (GHSA-3cgp-wmrg-4fqg residual), and the misleading
comment claiming the Electron seed-credential exemption is harmless is corrected
(GHSA-4r2r-mv32-3468).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(config): resolve signing secrets lazily so load order cannot split them
Found by a runtime probe against a locally built API: login returned 200 but the
access token it issued was rejected on the next request, because the process was
signing with one secret and verifying with another.
apps/api/src/main.ts calls loadEnv() (which reads .env.local and friends) only
after its imports have run, so @gauzy/config can be evaluated while JWT_SECRET is
still unset. With the published literal as the fallback that was invisible: the
early copy and any later copy both ended up on 'secretKey'. Once an unset secret
became a per-process random value, the early copy generated one while a copy
imported after loadEnv() read the configured value — so tokens signed by one
never verified in the other, and every authenticated request 401'd.
The four secrets are now getters on `environment` / `environment.prod` /
`defaultConfiguration.authOptions`, so each is read at first use, after the env
files are loaded. Adds the regression test for exactly that order.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(security): separate an unavailable account lookup from a dead refresh token
Review pass over PR #10240. The substantive fixes:
- MCP refresh grant (CodeRabbit major, Greptile P1). `getMcpUserInfo` caught
every error and answered `null`, so a database blip reached
`refreshAccessToken` as "this user is gone" and the grant handler answered
`invalid_grant`. Well-behaved OAuth clients discard a refresh token on that,
so one transient outage signed active users out for good. `getMcpUserInfo`
now re-throws a failed lookup, `refreshAccessToken` re-throws it as
`UserLookupUnavailableError` (outside its own catch, so it cannot be
flattened into `null`), and the grant answers `temporarily_unavailable` /
503. `null` now means exactly one thing: the account can no longer sign in.
The refresh token is still never revoked on either path.
- `resolveUser` is now REQUIRED (Greptile P2). It was optional on a public
method, so a caller could keep the two-argument shape and silently skip the
account check the parameter exists to perform. A missing resolver now throws
before anything else (500 server_error), matching the missing-provider path.
`UserLookupUnavailableError.is()` is used instead of bare `instanceof`: this
package ships both as source and as a bundle, and a downlevelled
`extends Error` would break the prototype chain and quietly restore the
invalid_grant behaviour.
- Nested credentials reached the desktop logs (CodeRabbit major, CWE-532).
`redactSecretsForLog` only looked at top-level keys, and `apps/desktop` logs
the whole `DesktopSetupConfig`, so `postgres.dbPassword` and
`secureProxy.ssl.key` were printed verbatim. It now walks nested objects and
arrays, with a depth cap and cycle detection.
- The seeded-account warning missed renamed installs (CodeRabbit major).
`getPublishedSeedAccounts()` read only today's `DEMO_*_EMAIL` values, but the
database was seeded in the past: an operator who changed the address after
installing still had `admin@ever.co` with the published password, and the
check walked past it. Both the configured and the canonical addresses are now
checked, deduplicated on the (email, password) pair.
- The bounded read could hide a vulnerable account (Greptile P2). One shared
`IN (...)` query with a global limit of 10 let the rows of whichever address
came back first use the whole budget. The budget is now per address (5 rows
each), so every candidate is actually looked at, and rows are re-matched
against the address they were fetched for.
- The lazy-getter commit (
|
||
|
|
a472bfa248 |
fix(security): CSV formula injection + rate-limit residuals (GHSA-7xp5, GHSA-86mw) (#10243)
* fix(security): neutralize spreadsheet formulas in CSV exports GHSA-7xp5-j564-4752 (medium): exported cells were written verbatim, so a stored value beginning with = + - or @ executed as a formula when a colleague opened the export in Excel or Sheets. A shared encoder in @gauzy/utils prefixes an apostrophe to any cell starting with a formula trigger (including the full-width forms), leaves strictly numeric values alone, and is reversed on import so an export/import round-trip stays byte-exact. Every field is now quoted, which also stops a bare CR inside a value from starting a new spreadsheet row, and the invoice CSV in the web app gets real RFC 4180 quoting instead of JSON.stringify. GHSA-86mw-2crg-vmhc residuals (low): the public invite routes are throttled, the shipped compose files no longer trust a forwarded client IP while publishing the API port directly, and RequestContext.currentIp() resolves the client address the same way the throttler does instead of reading a spoofable header. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(cspell): add the new vocabulary and use US spellings Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(export-import): decode CSV cells only for archives we marked The import side reversed the spreadsheet-formula escape on every parsed row, but an uploaded ZIP is not necessarily one this server wrote: it can be a dump from an older Gauzy, a filled-in `/export/template`, or a CSV set built by external tooling. For those, un-escaping is data loss — a legitimate value such as `'=notes` was persisted as `=notes`, silently. Both review bots flagged this as the one thing blocking the merge, and they were right: the decoder had no way to tell "we escaped this" from "somebody else wrote this". A data export now carries a `gauzy-export.json` marker at the archive root (format, version, `spreadsheetSafeCells`), written by `exportTables` and `exportSpecificTables`. `ImportService` resolves that marker once per import and decodes rows only when it is present and recognized; anything else is imported byte for byte as it was parsed. `/export/template` is deliberately NOT marked — an operator fills it in by hand, so nothing in it was ever escaped. The manifest reader is defensive about an attacker-supplied file: missing, oversized, malformed, a foreign format or a newer version all mean "do not decode". The invoice/payment CSV builder no longer has a path that skips encoding: a pre-joined header line used to be written through verbatim, and it was the only value in the file that reached disk unquoted and un-neutralized. `buildCsv` and `generateCsv` now declare `headers: string[]` (both callers already pass one), and a stray string from an untyped caller is split and encoded rather than trusted. Tests: `import.service.spec.ts` imports the same escaped CSV with and without a marker and asserts the apostrophe survives when unmarked (reverting the gate fails those 5 tests); `export.service.spec.ts` asserts the data archive carries the marker and the template archive does not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>v111.44.2 |
||
|
|
11722b0f5a |
chore(cspell): add tinyint to the dictionary
The MySQL column type appears in comments and a spec added by #10212. Existing uses were only inside packages/core/src/lib/database/migrations, which cspell ignores, so the word was never needed before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>v111.44.1 |
||
|
|
cf438f50b8 |
feat: allow employees to see their tracked data by default (#9874) (#10212)
Adds an organization setting `allowEmployeeToSeeTrackedData` (default true, so existing organizations keep today's behaviour) that lets an admin hide an employee's own tracked data — screenshots, activity, app/URL history, time logs, videos and statistics — from that employee. - `EmployeeTrackedDataGuard` enforces it on the read routes of the activity, custom-tracking, statistic, time-log and time-slot controllers and of the videos, camshot and soundshot plugins. It depends only on the global DataSource, so plugins can use it. - Exempt: admins (`CHANGE_SELECTED_EMPLOYEE`), callers with no employee record in the tenant, team/project managers of the organization, and the routes recording depends on — `time-slot/:id`, `time-log/:id`, `time-log/conflict`, `POST statistics/tasks` (the desktop timer's task picker) and every write route. A spec pins that list. - The setting is read from the caller's own organization plus any organization the request names, so a client-chosen `organizationId` cannot bypass it, and the employee identity is read from the database rather than the token claim. - `GET /timesheet/statistics/tracked-data-access` lets the web UI hide navigation with the same rule; the menus, the activity tabs and the dashboard widgets follow it, managers included. - Adds the admin toggle, seeds, the SQLite/PostgreSQL/MySQL migration and i18n for all 14 locales, and removes a UTF-8 BOM from 11 locale files that broke locale loading in the desktop apps. Resolves #9874. Co-authored-by: Meet Prajapati <meet987654@users.noreply.github.com>v111.44.0 |
||
|
|
0cc498b897 |
fix(candidate): keep billing rate cents instead of truncating to integers (#10235)
Follow-up to #10203, which fixed #10199 for employees only. - candidate.billRateValue / minimumBillingRate become numeric(14,2) / decimal(14,2) (migration 1790000016000; 1790000015000 is taken by open PR #10212). Postgres and MySQL alter both columns in one statement, Postgres with a 5 s lock_timeout; SQLite copies through a temporary column. reWeeklyLimit stays integer. - Candidate uses the same column options and toBillingRate transform as Employee, moved to shared/pipes/billing-rate.transform.ts so the two cannot drift. - Fixes a regression on develop: PUT /candidate/:id validates rates with the employee UpdateProfileDTO, which keeps cents since #10203, so Postgres rejected 10.49 in the integer candidate column (400) and MySQL rounded 10.5 to 11. - ColumnNumericTransformerPipe(scale).to() stores a blank string as NULL instead of returning 400 (routes that skip DTO validation, e.g. POST /candidate and /employee). - Specs: candidate transforms, UpdateCandidateDTO request path, response serialization, migration SQL for all engines, blank-string handling. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>v111.43.7 |
||
|
|
4c6e7c7fda |
fix(security): stop logging the Redis password and other credentials at boot (#10234)
The API printed the full REDIS_URL - Valkey password included - to stdout at boot (Redis health indicator and Redis session store). Boot-time log lines now go through dependency-free helpers in core/util/redact-credentials.ts that keep scheme/user/host/port and replace credentials with ***: - REDIS_URL lines (health indicator, session store): redis://:***@host:port - Malformed REDIS_URL: the ERR_INVALID_URL error (which carries the URL on error.input) is redacted before console.error; a partially redacted error is never returned - tracer: Honeycomb API key presence only; OTEL_EXPORTER_OTLP_HEADERS names only; tracing URL redacted - Unleash config line: customHeaders by name only, URL credentials redacted The Redis clients, exporters and Unleash still receive the real credentials; only log output changes. Specs cover every path with sentinel values and positive controls, and each was confirmed red against the previous code. No credential rotated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>v111.43.6 |
||
|
|
ca19d325c0 |
fix(candidate): keep billing rate cents instead of truncating to integers
Follow-up to #10203, which fixed #10199 for employees only. - candidate.billRateValue / minimumBillingRate become numeric(14,2) / decimal(14,2) (migration 1790000016000; 1790000015000 is taken by open PR #10212). Postgres and MySQL alter both columns in one statement, Postgres with a 5 s lock_timeout; SQLite copies through a temporary column. reWeeklyLimit stays integer. - Candidate uses the same column options and toBillingRate transform as Employee, moved to shared/pipes/billing-rate.transform.ts so the two cannot drift. - Fixes a regression on develop: PUT /candidate/:id validates rates with the employee UpdateProfileDTO, which keeps cents since #10203, so Postgres rejected 10.49 in the integer candidate column (400) and MySQL rounded 10.5 to 11. - ColumnNumericTransformerPipe(scale).to() stores a blank string as NULL instead of returning 400 (routes that skip DTO validation, e.g. POST /candidate and /employee). - Specs: candidate transforms, UpdateCandidateDTO request path, response serialization, migration SQL for all engines, blank-string handling. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
55eff103a0 |
fix(employee): keep billing rate cents instead of truncating to integers (#10203)
* fix(employee): keep billing rate cents instead of truncating to integers parseInt dropped decimals on billRateValue and minimumBillingRate, so 10.49 was stored as 10. Persist those money columns as numeric(10,2) and round to two decimal places. Weekly hour limits stay integer. Fixes #10199 * fix(employee): round billing rates and SQLite rollback to integers Address PR review: persist two-decimal money in the TypeORM transformer, use half-up rounding instead of toFixed, CAST ROUND on SQLite down, and assert ADD/UPDATE/DROP/RENAME order. * fix(employee): keep the full old rate range and keep rejecting non-numeric rates - numeric(10,2)/decimal(10,2) topped out at 99,999,999.99 while the old int columns held up to 2,147,483,647; use (14,2), as the payroll money columns do, so no value that saved before is rejected and the ALTER cannot overflow on existing rows - toBillingRate turned any non-numeric input into 0, so @IsNumber never failed; parse with parseFloat and return NaN for non-finite values so 'abc', true, {} and 'Infinity' are rejected again and '10,50' keeps 10 - roundToScale returned NaN for numbers printed in exponent form (1e-7) - specs for all three; prettier on the new migration Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(employee): refuse non-numeric rates instead of storing 0, round like the database - ColumnNumericTransformerPipe(scale).to() now parses strings like the DTO transform and throws 400 for anything that is not a finite number. POST /employee and /employee/bulk do not validate billRateValue / minimumBillingRate, so 'abc', '', true or {} used to be stored as 0 or 1 where the old integer column rejected them. - roundToScale rounds half away from zero (-1.005 -> -1.01), as Postgres numeric and MySQL decimal do; returns NaN rather than 0 for non-numeric input; and leaves doubles too large to hold cents unchanged instead of drifting or overflowing to Infinity. - Migration: Postgres and MySQL alter both columns in one statement (one table rewrite on Postgres, no half-applied state on MySQL), and Postgres sets a 5 s lock_timeout so a long transaction on employee cannot queue every request behind the ALTER. - Public-page employee form: bill rate step 0.1 -> 0.01, matching the rates form. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(employee): type the rate transformer input as unknown, clear static-analysis warnings - ColumnNumericTransformerPipe.to() takes `unknown`: routes that skip DTO validation pass raw request values through, so the string branch is reachable (DeepScan CONSTANT_CONDITION) - parsing moved into parseNumeric(); Number.parseFloat / Number.NaN (SonarCloud) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Ruslan Konviser <evereq@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>v111.43.5 |
||
|
|
33d464dba1 |
Merge pull request #10227 from joel-kalema/fix/polish-goals-pages
Fix/polish goals pagesv111.43.4 |
||
|
|
85e69c2236 |
Merge pull request #10231 from Ahson-Shaikh/add-easypanel-deploy-docs
Add Easypanel deployment optionv111.43.3 |
||
|
|
908f2fc004 | Add Easypanel deployment option | ||
|
|
24c6a4dbe2 |
Merge pull request #10225 from heykav/fix/is-class-instance-null-prototype
fix(utils): isClassInstance throws on a null-prototype objectv111.43.2 |
||
|
|
624355f8b7 |
fix(utils): don't throw when constructor itself is null or undefined
`{"constructor": null}` is valid JSON, so any parsed payload can carry an own
`constructor` that is null or undefined. Reading `.name` on it throws exactly
like the null-prototype case this branch fixes, and the crash is reachable from
real input: `parseObject`, `deepClone` and `deepMerge` all call
`isClassInstance`, and `{"constructor":null}` survives `JSON.parse` intact.
Read the constructor into a local first and treat null/undefined as plain data.
Behaviour is unchanged for every input that did not already throw - checked
against the pre-fix implementation across 27 object shapes (plain literals,
null-prototype objects, class instances, Date/Map/Set/RegExp, arrays,
primitives, nested values); only the previously-throwing shapes changed, and
all of them now answer `false`.
|
||
|
|
6f547325e9 |
Merge pull request #10226 from ever-co/chore/cspell-dedup-words
chore(cspell): add deduped, dedups, resaved and upsertv111.43.1 |
||
|
|
e51dc49ba0 |
chore(cspell): add deduped, dedups, resaved and upsert
The cspell job went red on develop after #10198 (run 35277570001, 9 issues in 5 files). All four are inflections of words the list already carries (dedup, resave, upserted/upserts). All 5 flagged files now pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d810c81cf3 |
feat(core): persistence invariants, config/boundary validation, migration smoke test and correlation IDs (#10198)
Adds test harnesses for tenant isolation, TypeORM/MikroORM parity, persistence invariants and idempotency; startup validation for DB config; Nx plugin/core boundary rules; a fresh-database migration smoke test; and a correlation id carried from HTTP requests into docs queue jobs and logs. Testing (packages/core/src/lib/core/testing): - tenant-isolation: an in-memory tenant-aware repository, fixtures and strict assertions (a non-empty page, every row in the caller's tenant). Applied to EmployeeService and OrganizationProjectService. - orm-conformance and persistence-invariants: suites that run the same checks under TypeORM and MikroORM (run-both-orms.sh uses yarn nx). - idempotency: assertion helpers and specs for token cleanup (positive control), employee notifications and the Zapier timer webhook. - database/migration-smoke.spec.ts: runs the full migration chain on a fresh better-sqlite3 database. It is excluded from the default core jest run; run it with `nx run core:test-migration-smoke`. Idempotency: - EmployeeNotificationService.create() takes an opt-in `absorbRedelivery`. With it set, an identical unread, unarchived notification under 60 s old (same receiver, entity, type, sender, title and message) is returned instead of inserted, and a missing key or a failed lookup inserts as usual. The event handler does not enable it (the in-process EventBus never redelivers), so every caller inserts one row per event as before. - ZapierWebhookService skips resending a webhook that already succeeded for the same subscription, action and time log within 5 minutes. The key is reserved while in flight, failed deliveries stay retryable, pruning stops at the first unexpired entry, and the cache is capped at 10,000 entries. Config (packages/config): - An unknown DB_TYPE fails fast with the list of supported values; an empty DB_TYPE still means better-sqlite3; mongodb throws an Error. - Pool and timeout variables are parsed with Number.parseInt semantics, only for postgres and mysql. They throw only where tarn already refused to start and warn otherwise. SQLite ignores them and still prints the startup values. Observability: - RequestContextMiddleware accepts an inbound x-correlation-id of 1-128 visible ASCII characters (otherwise it generates a UUIDv4) and echoes it on the response. CORS allows and exposes X-Correlation-Id. RequestContext.currentCorrelationId() is added. - The docs queue carries the correlation id through job payloads (including bulk reindex) into pipeline outcome, error, dead-letter and enqueue-failure logs. Boundaries and build: - Every project gets a type tag. The ESLint depConstraints stop type:core depending on plugins, and plugins may depend only on core, shared libs and the known extension points (ai-chat, job-proposal, integration-ai, job-*-ui). - core declares @gauzy/scheduler (package.json and implicitDependencies); the webapp Dockerfile copies scheduler's package.json. - The integration-zapier jest config can import @gauzy/core (transformIgnorePatterns, allowJs, isolatedModules). - Fixes the stale @nrwl/nx eslint-disable id in the e2e roles-permissions steps and the broken @gauzy/core mock in the docs document-scope spec. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>v111.43.0 |
||
|
|
f434f92c3b | test(utils): cover isClassInstance/deepClone/deepMerge with null-prototype objects | ||
|
|
ec96bafdc7 | fix(utils): isClassInstance throws on a null-prototype object | ||
|
|
da19222a45 |
Feat/polish timesheet details (#10221)
* style(timesheet): size the page title and breadcrumb at 12px * fix(timesheet): let each tab's card fill the page down to the footer * style(timesheet): tighten the filter row on the Calendar, Weekly and Daily tabs * style(timesheet): set all text on the Calendar tab to 12px * fix(timesheet): stop hour rows showing through the calendar's sticky day header * feat(timesheet): open the Daily selection actions in the filter row * style(timesheet): set Daily row text to 12px * refactor(timesheet): move the View page's inline styles into classes and drop a debug log * fix(timesheet): align View page rows with their header and mark the selected log * feat(time-log): lay out the Edit Time Log form on one shared grid * fix(time-log): return to the View Time Log popup when the edit is cancelled * fix(time-log): fit the View Time Log pane to its card so the backdrop closes it * feat(time-log): redesign the View Time Logs popover with a header, aligned rows and a footer * fix(timesheet): cap the Weekly tab's text at 12px like the Calendar tab * feat(timesheet): redesign the screenshot slot popup with a header, summary strip and aligned time logs * fix(timesheet): give the slot popup's screenshots an employee id so the viewer opens on the clicked one * feat(timesheet): move the slot popup's summary into its header and put apps beside their title * style(timesheet): fit the slot popup without a scrollbar and match its thumbnails and bar to the Recent Activities card * fix(gallery): open the screenshot viewer without focusing its first button * feat(gallery): redesign the screenshot viewer with a top bar, round nav buttons and a filmstrip * style(gallery): darken and blur the viewer backdrop and dim missing-screenshot placeholders * feat(gallery): sort the viewer by time, step with arrow keys and keep the active thumbnail in view * feat(i18n): add screenshot labels that name each screenshot by its time or place in the set * fix(timesheet): give the slot popup's screenshots alt text and name their info button * fix(timesheet): let the Time Logs popover's time range wrap on narrow screens * fix(gallery): name the viewer's info button, image and thumbnails and mark the selected thumbnail * fix(gallery): scroll the filmstrip without animation when reduced motion is onv111.42.17 |
||
|
|
73e2318a7b |
Merge pull request #10222 from ever-co/fix/desktop-apps-missing-style-tokens
fix(desktop): mirror ui-core style tokens in desktop apps' shadowing SCSS copies (build-desktop red since 09-13)v111.42.16 |
||
|
|
abd838e202 |
fix(desktop): mirror ui-core style tokens in desktop apps' shadowing SCSS copies
The desktop-side Angular apps (desktop, desktop-timer, server, server-api,
agent) put apps/<app>/src/assets/styles ahead of
packages/ui-core/static/styles in stylePreprocessorOptions.includePaths.
Their own var.scss and gauzy/_gauzy-overrides.scss therefore shadow the
ui-core files for every bare `@use 'var'` / `@use 'gauzy/_gauzy-overrides'`
in shared ui-core stylesheets, and tokens added only to ui-core are
undefined in these builds:
- $tabset-dsk-tab-padding-block / -inline (#10160), read by
includes/_tabset.scss and gauzy/_tabset-actions.scss
- $control-hairline / $control-padding-x (#9857), read by
time-tracker.component.scss since #10207
This has failed build-desktop on develop since
|
||
|
|
24ff0e3db0 |
Merge pull request #10219 from ever-co/chore/cspell-dotfile-words
chore: add dotfile, dotfiles and unmocked to the spell-check dictionaryv111.42.14 |
||
|
|
8c145d3171 |
Merge pull request #10195 from ever-co/fix/ghsa-mggh-w3mx-webhook-and-ssrf
Fix: verify GitHub webhook signatures and guard AI-provider base URLs against SSRFv111.42.13 |
||
|
|
9c866c72e1 |
fix(security): cover reserved address ranges and harden the connect-time lookup
Three follow-ups from the independent verification of the connection-level SSRF guard. The private-address predicate in @gauzy/utils treated several special-purpose ranges as public: benchmarking space 198.18.0.0/15 (a common cluster CIDR), the IETF and documentation ranges, multicast, 240.0.0.0/4, deprecated site-local fec0::/10, the NAT64 local-use prefix 64:ff9b:1::/48 and the deprecated IPv4-compatible ::a.b.c.d form, so [::7f00:1] passed the literal check. They are now refused. The shared predicate is also used by the core SSRF agent and the Make.com, Zapier and Ever Async integrations; none of their legitimate destinations sits in these ranges. The validating lookup's promise chain now ends in a catch. If Node's connect callback ever threw synchronously, the throw would otherwise become an unhandled rejection, which terminates the process on Node 24. An empty constructor in a spec mock tripped the no-empty-function lint rule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1d6f28cec0 |
test(ai-chat): cover connection-level bypasses of the AI-provider SSRF guard
Add ssrf-connection-bypass.spec.ts. It runs against a real HTTP server on 127.0.0.1 that a bypass would actually reach, and covers: - The default dns.lookup path, with no injected resolver. A name that answers public, or "no such host", to the pre-flight and loopback at connect time is refused. localhost as the OS resolves it is refused, and an operator endpoint on localhost still connects. - IP literals that Node connects to without calling the lookup: decimal, hexadecimal, octal, shortened, IPv4-mapped, the unspecified IPv6 address and 0.0.0.0. Each is refused before a socket opens. - An HTTPS request resolves through the lookup too. - A Happy Eyeballs answer set that hides one private address among public ones is refused as a whole. - A keep-alive server gets a new connection, and a new lookup, for every request. - Hostile upstream behaviour raises no unhandled error: a reset before the headers, during the body or of a body nobody reads, a corrupt gzip body, an abort with the body unread, and a streamed upload refused at connect. Each group fails when its guard is broken (lookup check off, first address only, literal check off, pooled agent, no request error listener). The existing suites stayed green with a pooled agent and with the default connection resolver bypassed, so those two properties had no coverage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |