Commit Graph

7 Commits

Author SHA1 Message Date
NgaiYeanCoi 1d85ef138e fix(sub): prevent default profile page URL disclosure (#6538)
* fix(sub): prevent default profile page URL disclosure

Add explicit none, builtin, and custom profile page modes.
Preserve existing custom URLs and warn before exposing the built-in page.
Cover mode selection, legacy settings, and subscription response headers.

* fix(subscription): add profile page link options and upgrade notes
2026-09-15 21:13:29 +02:00
NgaiYeanCoi 6a5b4fab6a feat(happ): generate Crypt5 subscription links locally (#6494)
* feat(clients): add stateless Happ link generator

Generate Happ provider links from the current effective subscription source without caching results. Reject unsafe provider responses and redact failure diagnostics.

* fix(clients): reject duplicate Happ provider fields

Parse Happ provider objects token by token so duplicate supported keys cannot be silently overwritten by encoding/json.

* feat(clients): expose on-demand Happ link API

Expose a no-store client endpoint backed by the Happ link generator and keep its generated OpenAPI contract synchronized.

* fix(openapi): exclude service interfaces from generated types

Keep dependency-injection interfaces out of the frontend API surface while preserving allowed response schemas.

* feat(clients): add stateless Happ QR presentation

Generate Happ links only for the active modal scope and retire late responses so Standard remains immediately available. Add focused component coverage and localized retry guidance across every locale.

* fix(clients): cover overlapping Happ generations

Prove the cancellation cleanup is required by resolving a retired request while its replacement remains pending. Also wait for Regenerate to leave loading state before exercising the existing action.

* fix(clients): harden Happ link handling

Validate generated responses before rendering and hide actions during unresolved requests. Strengthen route, redirect, timeout, and lint regression coverage with mutation-sensitive tests.

* fix(clients): gate Happ link generation behind operator opt-in

- add a fail-closed happLinkEnable setting
- enforce the gate before and after provider requests
- add locked Happ QR state with privacy disclosure and settings link
- cover backend, frontend, settings, and i18n regressions

* fix(frontend): guard oversized Happ QR codes

Keep valid long crypt5 links copyable while suppressing QR rendering and image actions above the encoder's UTF-8 byte limit. Add localized guidance and boundary coverage.

* fix(clients): log the sanitized transport error for Happ link failures

Every fail() call in HappService.Generate passed a string literal as the
detail, so the sanitizer written for provider errors only ever saw
constants, and an operator following the QR modal's "check Logs" hint
found nothing beyond reason=transport. Transport and body-read errors now
flow through sanitizeHappDetail, which also redacts cookie/session pairs.

Drop TestHappLinkEnableDefaultsOffWithoutPersistingRow: it pinned a getter
and its constant default, which the Generate gate test already drives.

* fix(frontend): size the Happ QR cap to level L and keep the QR modal mounted on close

HAPP_QR_MAX_BYTES was the level-M capacity (2331) while QrPanel encodes at
errorLevel "L", whose version-40 byte-mode capacity is 2953, so valid links
between 2332 and 2953 bytes lost their QR. The cap now matches the encoder
and a test renders the real QrPanel at the boundary.

Keying the modal content on `open` remounted it on every close, which cut
the Modal's exit transition and made the openSubId sync unreachable, so
`loading` never turned on for the subLinks fetch and a client without a
subscription link flashed noLinks on reopen. `open` leaves the key and the
sync block now also resets the Happ state.

* chore(clients): request Happ crypt5 links from api-v3

crypto.happ.su serves api-v2.php and api-v3.php side by side. Probed with
the same payloads, both take {"url"} over a JSON POST, answer
{"encrypted_link":"happ://crypt5/..."} of identical length with the same
crypt5 key marker, and fail the same way: 400 "No url provided.",
500 "Invalid URL format.", 405 on GET. Happ's own generator page is
branded "URL Encryption v3", so the panel follows it. The parser and the
link validator are unchanged.

* feat: add local generation of encrypted Happ links

- Implemented functionality to generate encrypted Happ links locally without network dependency.
- Added validation for URL length and format to ensure compliance with processing limits.
- Introduced new error handling for invalid URLs and control characters.
- Updated translations for various languages to reflect changes in Happ link generation.
- Created unit tests to validate the encryption process and ensure session keys and nonces are unique.

* fix(frontend): match the tuic memo deps to the non-optional subSettings

The Happ branch reads subSettings non-optionally in ClientQrModalContent
(happLinkEnable and the WireGuard/AmneziaWG publicHost memos), so React
Compiler infers subSettings.publicHost. The TUIC memo merged in from main
still listed subSettings?.publicHost, which fails oxlint's
preserve-manual-memoization rule and makes the compiler skip optimizing
the component. make verify stopped at lint-fe on the branch head.

* chore(happ): trim the pinned-key provenance comment to two lines

CLAUDE.md caps a comment block at two lines. The bare URL line repeated
the repository and file the next line already names, so it is folded
into that line (review LOW on happ_crypto.go).

---------

Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
2026-09-13 12:44:55 +02:00
Sanaei 92fb94d856 Move to TypeScript 7 and the oxc toolchain (oxlint + oxfmt) (#6262)
* chore(frontend,docs): move to TypeScript 7 and replace ESLint with oxlint

TypeScript 7 is the native Go port and ships no programmatic compiler
API, so typescript-eslint cannot run at all: it peer-pins
typescript >=4.8.4 <6.1.0 (canary too) and hard-crashes with
"typescript-eslint does not support TS 7.0". Upstream support is
tracked in typescript-eslint#10940 and targets TS >=7.1.

Rather than wait, or carry Microsoft's side-by-side alias (which keeps
a second TS 6 install alive purely to feed the linter), both projects
move to oxlint, which never depended on the TypeScript API.

Typecheck drops from ~9.7s to ~2.2s and 167 packages leave frontend/.

oxlint has no no-restricted-syntax, so the #6121/#6127 cleared-
InputNumber guard is reimplemented as a JS plugin in
frontend/tools/oxlint/. It was verified to still fire in
pages/settings/** and pages/xray/** and to stay exempt in *Modal.tsx.

The type-aware @deprecated sweep survives too, as
`npm run lint:deprecated`: oxlint's type-aware mode runs on
oxlint-tsgolint, which drives the TS 7 typescript-go checker, so the
TS 7 move is what makes it possible.

Behaviour is preserved rather than tightened. jsx-a11y/prefer-tag-over-role
is off in both configs because it was never part of the recommended sets
ESLint actually ran, and oxlint honours the existing eslint-disable
comments, so no source churn was needed.

Two real fixes fell out of the stricter linting:
- outbound-link-parser.test.ts used `out?.streamSettings` behind an `as`
  cast, which hid the optional chain from ESLint and would throw on a
  null parse; the rest of the file already used `out!`.
- InputAddon's conditional role/tabIndex/onKeyDown is genuinely
  accessible but oxlint cannot evaluate it, so it gets a scoped disable.

* chore(docs): replace Prettier with oxfmt

oxfmt is the oxc project's Prettier-compatible formatter, so this pairs
with the oxlint move and drops the last JS-based tool from the docs
toolchain.

The swap is behaviour-preserving. Running Prettier and oxfmt over the
same files, with the existing .prettierrc.json settings migrated via
`oxfmt --migrate=prettier`, produces byte-identical output on every
file. (Comparing them outside the project directory is misleading:
Prettier silently falls back to its defaults when it cannot find its
config, which looks like a mismatch but is not one.)

The 18 files reformatted here were already failing `pnpm format:check`
before this change — Prettier wanted the exact same edits. The check is
not part of docs-ci.yml, which is why the drift went unnoticed.

.prettierignore becomes ignorePatterns in .oxfmtrc.json, keeping the
deliberate MDX exclusion: reflowing MDX prose merges headings into
paragraphs and collapses lists inside Steps/Callout components. Both
that and the generated fumadocs-openapi reference output were verified
untouched.

oxfmt is pinned to 0.63.0 rather than latest. pnpm 11's built-in
minimumReleaseAge policy rejects same-day releases, and 0.64.0 would
have made pnpm silently append 20 waiver lines to pnpm-workspace.yaml.

* style(frontend): adopt oxfmt and format src

frontend/ has never had a formatter, so this reformats 344 of 497 files
in src/. The change is purely whitespace, quoting and line wrapping —
no logic is touched. It is kept in its own commit so it does not bury
the TypeScript 7 / oxlint migration or the git blame for the code
itself.

Settings match docs/ and the code as it was already written: single
quotes, semicolons, trailing commas, 2-space indent, 100 columns. That
was measured rather than assumed — src/ was already uniformly
single-quoted and 2-space indented, with p90 line length at 75.

Formatting is scoped to src/ (mirroring `oxlint src`) and
.oxfmtrc.json ignores src/generated. Both matter: `make gen-check`
compares src/generated and public/openapi.json, and
`make msw-worker-check` byte-compares public/mockServiceWorker.js
against the installed MSW runtime, so reformatting any of them breaks
the gate.

Reflowing also moves `eslint-disable-next-line` comments off the line
they guard, which broke two suppressions that had been silently
correct before:
- clone-inbound-modal.test.tsx: the object literal became multi-line,
  leaving `} as any;` four lines below its no-explicit-any disable.
- ClientsPage.tsx: the useMemo dependency array moved onto its own
  line, out from under its exhaustive-deps disable.
Both comments were relocated onto the line they actually guard, and
verified to still suppress by removing them and watching the errors
return.

* ci: enforce formatting in CI and make verify

Adding oxfmt in the previous two commits gave both projects a formatter
but nothing that checks it, which is how docs/ had already drifted to 18
unformatted files: docs-ci.yml runs typecheck, lint, test and build, but
never format:check, so Prettier's complaints were only ever visible to
whoever ran it by hand.

Wire `format:check` into the frontend job in ci.yml and the docs job in
docs-ci.yml, and add a `format-check` target to `make verify` so the
local gate keeps mirroring CI as the Makefile header promises.

Verified the step actually bites rather than passing vacuously: adding
a badly formatted line to a source file in each project makes both
`make format-check` and `pnpm format:check` fail, and reverting it makes
them pass again.

No workflow referenced ESLint or Prettier by name — they all invoke the
package scripts — so the tooling swap needed no other CI changes.

* ci: trigger CI on Makefile changes

The path filters listed **.go, go.mod, go.sum, frontend/**, .nvmrc and
ci.yml itself, but not the Makefile — so a change to the canonical task
runner that ci.yml is meant to mirror could land without any job
running. The previous commit, which edits both, only triggers because
it happens to touch ci.yml too.

* fix(frontend): replace deprecated Ant Design 6 APIs in the geo components

`npm run lint:deprecated` reported five uses of props Ant Design 6 has
deprecated. All five are gone, and the matching runtime warnings no
longer appear in the test output.

Tag `bordered={false}` becomes `variant="filled"` and Space `direction`
becomes `orientation`; both are the one-to-one replacements named in
antd's own deprecation messages, and `direction`/`orientation` share the
same Orientation type.

Input `addonAfter` is the one that is not a rename. It becomes a
`Space.Compact block` wrapping the Input and the browse Button, which is
antd's documented migration. `block` keeps the field filling its form
row as the addon did. Note this is a deliberate visual change: the
button used to be a borderless `type="text"` icon sitting inside the
addon's grey box, and is now a regular button whose border joins the
input. The tooltip, aria-label, ref, id and onBlur wiring are unchanged,
so the react-hook-form binding in RuleFormModal and the existing tests
still address it the same way.

Only these five were deprecated. The other `bordered` props in the tree
sit on QRCode, Table, Descriptions and Alert, where the prop is not
deprecated, and these were the only two Space `direction` uses in the
codebase.

* fix(frontend): restore lint rules lost in the oxlint migration, and test the guard

Addresses the review on #6262.

The frontend config re-enabled only no-explicit-any and no-unused-vars
and left the rest of tseslint's recommended set to oxlint's correctness
category. It does not cover all of it. Confirmed by linting one probe
file against both configs: docs/ (which enumerates the rules) reports
all nine, frontend/ reported four. So ban-ts-comment,
no-empty-object-type, no-namespace, no-require-imports and
no-unsafe-function-type had silently stopped being enforced — a `//
@ts-ignore` or a `namespace` block would have landed unflagged. The ten
rules are now mirrored from docs/.oxlintrc.json, and src/ still passes.

The #6121/#6127 guard was 57 lines of hand-written AST walking with no
test. It now has one: fixtures for the three banned shapes plus an
onNumber()-wrapped control, asserting the rule fires three times and
that .oxlintrc.json still wires it to the right paths. Verified it fails
for the right reason by making walk() enumerate nothing, which is the
silent-death mode the review described — the traversal depends on
Object.keys() seeing AST children as own enumerable properties.

The fixtures deliberately violate the rule, so their oxlint config is
named guard.oxlintrc.json rather than .oxlintrc.json: oxlint discovers
nested configs by directory, which would otherwise turn the fixtures
into three lint errors. The test passes it explicitly with -c.

Also from the review:
- lint and format now cover tools/ as well as src/, so the one piece of
  hand-written lint logic in the repo is no longer the least covered
  file in it.
- lint-staged runs oxfmt before oxlint --fix. Formatting became a hard
  CI gate in this PR while the hook only ran the linter, so a commit
  could pass the hook and fail CI on formatting alone.
- .oxfmtrc.json ignores public/, so the artefacts that make gen-check
  and make msw-worker-check byte-compare stay safe even if oxfmt is
  invoked without a path argument.
- The MDX and generated-reference rationales that .prettierignore
  carried are back as comments in docs/.oxfmtrc.json — oxlint and oxfmt
  both accept JSONC, so relocating them was unnecessary.

Not applied: the review also suggested restoring ../internal/web/dist to
the ignore lists. Both tools reject `..` patterns outright ("patterns
are resolved within the config file's directory"), and being outside
frontend/ it is unreachable anyway.
2026-08-19 15:36:27 +02:00
PathGao 411271b454 refactor(ui): share one onNumber handler for numeric setting inputs (#6127)
* refactor(ui): share one onNumber handler for numeric setting inputs

The Number(v) || 0 idiom in InputNumber onChange handlers is the root
pattern behind the cleared-port bug (#6121): AntD reports a cleared
field as null, and || 0 turns that into a stored zero or a min-clamp.
The port fields got an inline null-guard; the other sixteen numeric
settings kept the idiom, so every new field is a chance to
reintroduce the bug.

Extract the guard into onNumber(apply): null, empty and NaN change
events are ignored so a cleared field snaps back to its stored value
on blur, and numeric events pass through unchanged. Convert all
sixteen sites in the settings and xray pages. Two sites keep their
deliberate different semantics: smtpPort falls back to 587 on clear,
and the Telegram notify interval clamps through Math.max.

For the non-port fields this changes clearing from storing 0 to
keeping the stored value; zero remains reachable by typing it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(ui): fold the remaining hand-rolled numeric guards into onNumber

From review: ObservatorySettingsTab's sampling field hand-rolled the
same ignore-null semantic and smtpPort kept a fallback-to-587 on
clear that nothing documents as intentional and that silently
overwrites a configured non-standard port — both now go through the
shared helper, leaving the Telegram interval clamp as the one
deliberate exception.

Also from review: narrow the helper to numbers only (no stringMode
input exists in the repo, and the string branch codified a guarantee
the number-typed callback cannot honour), soften the docblock to
describe behavior rather than promise prevention, add a GeneralTab
component test covering the clear-vs-typed-zero semantics, and assert
the blur snap-back in both settings tests so a display/state desync
cannot ship unnoticed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 23:03:13 +02:00
PathGao 579acbc669 fix(settings): keep the stored port when a port field is cleared (#6121)
* fix(settings): keep the stored port when a port field is cleared

Clearing the panel-port, subscription-port or LDAP-port InputNumber
fired onChange(null), which the handlers coerced to 0; on blur Ant
Design clamped the empty field to min=1 and the next save silently
persisted port 1. For subPort that breaks the generated subscription
links; for webPort it moves the panel itself to port 1 and locks the
admin out until the port is fixed via the x-ui CLI.

Ignore null changes so clearing a port field snaps back to the last
valid value instead of committing a bogus port.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(settings): pin cleared port fields to the stored value

Clearing the subscription-port field must not reach updateSetting at
all, while typed ports still pass through unchanged. Pins the fix so a
handler refactor cannot silently reintroduce the clamped port 1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 20:10:21 +02:00
Sanaei edb487a005 chore(deps): migrate to react-router 8 and refresh frontend dependencies
react-router-dom 7 is superseded by react-router 8, which folds the DOM
bindings back into the core package. RouterProvider now comes from
`react-router/dom`, while the hooks and `createBrowserRouter` move to
`react-router`. Updates the nine importing modules and the router line in
docs/architecture.md to match.

Also refreshes antd, react-i18next, storybook, eslint, lint-staged and
playwright to current patch/minor releases, and restores alphabetical order
in devDependencies for the @vitest/browser-playwright and playwright entries.

Bumps brace-expansion to 5.0.8, the only release outside the affected range
of GHSA-mh99-v99m-4gvg (unbounded expansion length causing an OOM crash).
`npm audit fix` could not apply this on its own: the lockfile pinned 5.0.7
and npm will not re-resolve a transitive-only dependency in place, so the
entry was updated directly and reinstalled.
2026-07-25 02:07:15 +02:00
Tomi lla 129f50d92a feat(sub): auto-detect subscription format by User-Agent (Updated) (#5826)
* feat(settings): add subscription format controls

* feat(sub): auto-detect subscription formats

* fix(xray): validate balancer regexes before save

* Revert "fix(xray): validate balancer regexes before save"

This reverts commit 8a208ce71b.

* doc(endpoints): align indent spaces

* doc(settings): improve error message formatting in validateSubUserAgentRegex

- Use NewErrorf with proper formatting instead of NewError with string concatenation
- Add comment explaining the rationale for returning original pattern value
- This preserves the intentional design where empty input is stored as empty
  in the DB and inherited as the runtime default at read time

---------

Co-authored-by: Tomilla <5007859+Tomilla@users.noreply.github.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
2026-07-14 13:01:40 +02:00