Resend only runs after an explicit form submit, so a per-address
cooldown is unnecessary overhead compared with forgot-password.
Signed-off-by: Émile Ré <emile@probo.com>
Add a per-address confirmation-email cooldown and disable the
resend/forgot-password submit buttons while the mutation is in
flight so callers cannot flood the mail queue or double-submit.
Signed-off-by: Émile Ré <emile@probo.com>
Unverified password identities were able to open sessions after
signing out. Reject sign-in with EMAIL_NOT_VERIFIED and add a
resend-confirmation flow so users can complete verification.
Signed-off-by: Émile Ré <emile@probo.com>
Swallow locale mutation rejections after the toast, close the
mobile drawer on locale change, escape SEO paths, share the
IAM locale list with SEO, and finish dropping /trust leftovers.
Signed-off-by: Émile Ré <emile@probo.com>
Path-segment locales make each language crawlable with self
canonical and hreflang, while identity.locale persists an
explicit choice without client storage or cookie banners.
Signed-off-by: Émile Ré <emile@probo.com>
The source headers, LICENSE files, and license metadata had drifted
apart. Align the entire project to MIT:
- Convert every source-file header to the MIT text across all comment
styles (Go, TS, TSX, JS, MJS, SQL, CSS, GraphQL, shell), including
SPDX-License-Identifier tags
- Set the root and cookie-banner LICENSE files to the MIT text with a
"MIT License" title line
- Switch the package.json license fields, Docker image label, and
cookie-banner README to MIT
- Update docs and the genmodels header generator accordingly
- Normalize copyright lines to a single format
(Copyright (c) <year(s)> Probo Inc <hello@probo.com>.): unify the
hello@getprobo.com and hello@probo.inc emails to hello@probo.com and
the comma-separated years to a hyphenated range
Genuine third-party references are intentionally left untouched: the
Lucide icon attributions (Lucide is ISC) and the trivy dependency
license allowlist.
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
Unbounded LoadAll* loaders materialised an entire result set in one
query with no ceiling. A table that is small in development can grow
without bound in production, so these loaders were a latent memory
and query-time hazard.
Remove the LoadAll* methods from pkg/coredata and walk the cursor-
paginated LoadBy* siblings instead through a shared page.LoadAll
helper. The helper advances a MaxCursorSize forward cursor until the
result set is exhausted and concatenates the pages. It caps a single
call at MaxLoadAllPages (20) batches of 500 rows and errors past that
rather than materialising an unbounded set, so a runaway caller fails
loudly instead of exhausting memory.
Callers that genuinely need every row now express that explicitly,
and the coredata load-naming rule and docs are updated to discourage
new unbounded loaders.
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
Expose viewer.invitingOrganizations: [Organization!]! returning the
organizations that have a live pending invitation directed at the
current identity (accepted_at IS NULL AND expires_at > NOW()). The
list is rendered under a "Pending invitations" section on the
memberships page and in the organization selector dropdown, so a user
already signed in with an existing identity can see which
organizations have invited them without having to dig through their
inbox.
The new field is gated by iam:invitation:list against the viewer's
own identity, so it does not loosen authorization on Organization
elsewhere. E2E coverage validates the live-pending case, the
no-invitation and post-accept cases, and a multi-org scenario.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Session validity was decoupled from credential rotation: changing
a password (logged-in flow) or completing a forgot-password reset
left every existing iam_sessions row valid until its idle TTL.
A user who saw their account compromised on another device had
no way to actually evict that device by rotating the password.
Inside the same DB transaction as the password update, expire the
identity's other active sessions:
- ChangePassword keeps the caller's current session and revokes
every other session for the identity, so the user is not
logged out of the browser they just used.
- ResetPassword has no caller session (the user is anonymous,
authenticated only by a stateless token) and revokes all of
the identity's active sessions.
The session middleware already rejects rows with expire_reason
set, so revoked sessions are kicked out on the next request
without any middleware change.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>