6 Commits

Author SHA1 Message Date
mash2k3 856c13b09c Invite a client to register instead of handing them a password (#1780)
Staff can now invite a specific address to register instead of typing a
password for somebody and finding a way to get it to them. The invited
person sets their own, the link is locked to the address it was sent to,
and an invitation always activates the account regardless of the
auto-approve setting -- naming an address is already the decision the
approval queue exists to make for one nobody named.

Two fixes ride along: outgoing mail now reads the installation's own site
name in its title, header and signature rather than the one baked into
config('app.name') at install time, and the CSRF cookie name is read per
request rather than captured once at load.

Follow-up work, tracked separately: an invitation cannot be cancelled --
there is no pending-invitations screen and no revoke, so letting one expire
is the only way to take it back, which the self-service resend button then
undoes. Redemption also needs the address-availability check every other
non-form caller of ClientProvisioning makes.

Thanks @mash2k3.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPk8qAs38pudYGWwmGkYPe
2026-09-12 14:28:47 -03:00
denkfabrik-li 7264c44fd7 Serve the interface font from the installation, not from a font CDN
app.blade.php is the root template for all three interfaces, and it opened
with two lines pointing at a third party:

    <link rel="preconnect" href="https://fonts.bunny.net">
    <link href="https://fonts.bunny.net/css?family=instrument-sans:400,500,600" rel="stylesheet" />

Every visitor to /login, /register, /forgot-password, /s/{token} and every
public listing page therefore made a request to a host the operator did
not choose and could not switch off, before they had done anything at all
-- handing it their IP address, their user agent, and through Origin the
hostname of the installation they were visiting. On the signed-out pages
that is a visitor who has agreed to nothing, and an operator who often has
told their own users that this server is where their files live.

There was no self-hosted copy in the repository, no setting, no mention in
INSTALL.md, DOCKER.md or SECURITY.md, and no SRI on the tag.

The font now ships with the application, through @fontsource/instrument-sans
-- the same font, the same three weights the URL asked for, from a
versioned dependency rather than binaries pasted into the repository.
Vite fingerprints and emits them like any other asset.

Cost, measured on this build: twelve files, 192 KB on disk. A browser
fetches only woff2 and only the subsets it needs, which is 73 KB for all
six woff2 files together and typically 41 KB (latin, three weights) for a
page in English. Against that, every page load loses a DNS lookup, a TLS
handshake and a round trip to another origin, so signed-out pages get
faster rather than slower.

This is a privacy change rather than a vulnerability fix, and worth saying
plainly: the share token does not leak this way. Referrer-Policy:
strict-origin-when-cross-origin is set in both nginx configs and in the
INSTALL.md snippet, so the path never travelled in the Referer. What
travelled was the visit itself.

Not changed: public/.htaccess still sets no security headers at all, so an
Apache installation has no Referrer-Policy. That is a real gap and a
separate change.

Verified: `npm run build` succeeds and emits the faces; no reference to
the CDN survives anywhere in public/build; `tsc --noEmit` and prettier are
clean. No test asserts on the font, before or after.
2026-08-29 00:50:02 +02:00
ignacionelson 351da21e8d Stop the text half of an email printing its link twice in brackets
Laravel's notification view writes the subcopy URL as [$url]($url).
The HTML half parses that into an anchor; the text half parses nothing,
so it arrives as literal brackets around a duplicated address. With the
button line above it the URL appeared three times in one message.

It reads as broken, and it reads broken in a specific direction: a long
opaque token, the recipient's address in the query string, and a
duplicated link in brackets is the shape of a phishing template. On a
password reset, which is often the first mail an installation ever sends
somebody, from a domain with no reputation yet.

Fixed the way every other component in that message already handles the
same split -- one name, two files, Laravel picks per half. Which meant
publishing the framework's view for a one-line change, so there is a note
in it saying to re-copy on upgrade.

Seen in a real reset mail, not in a test.
2026-08-27 15:29:25 -03:00
ignacionelson a8a7f3f340 Send each edition to its own front door, and stop asking payers for money
Two things on the About screen were written when there was only one
edition. "Website" pointed at projectsend.org for everybody, and the
donation link was offered to hosted customers who are already paying for
this — on the same screen that thanks them for choosing it.

projectsend.org is the way in for the software you run yourself and
projectsend.cloud is the way in for the hosted service, so `links.website`
now resolves to whichever one the reader is actually using. That reaches
further than About by design: the "Powered by ProjectSend" line at the
foot of every outgoing email and on every client-facing page is where a
recipient meets this product for the first time, and sending a hosted
customer's recipients to self-hosting instructions is the wrong door.

The donation link is *omitted* rather than hidden by the page, so a
surface added later cannot ask a paying customer for money by forgetting
to check. Its TypeScript type is optional now, which makes the compiler
enforce the same thing.

Also fixed on the way past: the settings footer hardcoded the text
"projectsend.org" next to that link, so on the hosted service it named a
site it did not link to. It reads the host off the resolved URL now.

Verified in a browser against both editions, not only in tests. Cloud:
projectsend.cloud, no donation link, on both screens. Community:
projectsend.org and Open Collective, exactly as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 00:55:41 -03:00
Ignacio Nelson 0e3f7d5c68 Take the browser tab's name from the site, not from the build (#1621)
The tab title suffix came from `import.meta.env.VITE_APP_NAME`, with the
starter kit's own `|| 'Laravel'` behind it. Vite resolves that at build
time, and a release ships public/build/ already compiled — precisely so
nobody downstream needs npm — so whatever the build machine had is
frozen for every install that artifact produces. The 2.0.0 zip has
`const s2="Laravel"` in app-BoPagLMd.js, and no APP_KEY, APP_NAME or
VITE_APP_NAME an operator sets afterwards can reach it (#1619).

Swapping the fallback to 'ProjectSend' would fix the reported symptom
and leave the mechanism: a name baked at build time, wrong for anyone
who renamed their site. The name is already on every page — the shared
props carry the site_name setting — so read it from there and drop the
build-time variable entirely, .env.example's dead VITE_APP_NAME line
included.

The root view's own <title> now reads the same shared prop, so the tab
does not show one name before hydration and another after on any
installation whose administrator renamed the site.

Verified in a real browser, which is the only place this is visible —
the server-rendered title was always right, and curl never saw the bug.
Titles read "Log in - ProjectSend" and, after renaming the site with no
rebuild of any kind, "Log in - Acme Files".

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 12:42:06 -03:00
ignacionelson 6e47d76ba6 ProjectSend 2.0.0
Client file sharing, rebuilt from the ground up: a private area per
client, resumable uploads, folders, groups and categories, sharing with
expiry dates and download limits, comments, file versions, an activity
log, a REST API, and sixteen languages.

This repository begins here. ProjectSend 2 was developed privately, and
that development history is not published — the previous generation
remains available, with its own history, at projectsend/legacy.

Free software under the GNU General Public License v2, or (at your
option) any later version.
2026-08-14 01:38:12 -03:00