- TestOAuthProviders: old bytes without a source, order, per-request lookup
- TestOAuthIdentitiesDestroyRule: the PHP last-identity rule table, 404s,
microsoft by default, source names and opts.Providers precedence
- TestOAuthIdentitiesDestroyRace: two row-locked transactions in both orders,
the handler waiting for the lock, concurrent handlers
- TestRegisterUserPasswordLess, TestFinishLogin, TestRegistrationExports
- without a published *gorm.DB the command opens one with lagoon.OpenFromApp
for its run and closes it, as the plugin's other commands do (withDB)
- TestBackfillHandlesOpensFromConfigWhenUnpublished; README CLI note
- OAuthIdentitiesDestroy locks the users row (SELECT ... FOR UPDATE) and runs
the PHP guarded DELETE in one transaction: a self-set password or another
identity allows the unlink; otherwise 409 with the unchanged body
- the default allow-list adds microsoft, and an OAuthProviderSource's known
names are used when the host passes no Providers
- TestOAuthIdentitiesDestroyLastIdentityWithPassword (204),
TestOAuthIdentitiesDestroyConcurrentSmoke; README
- gofmt of the forgotPasswordBody line touched in the previous commit
- classes.OAuthProvider and classes.OAuthProviderSource; GET /oauth-providers
looks the source up per request and keeps the empty body without one
- exported RegistrationSettings, RegistrationsFromIP, RefreshTTL, RemoteIP
- RegisterOptions.PasswordLess (has_self_set_password=false) and FinishLogin
(restore + reactivate mail, last_ip_address, last_seen; no event)
- TestOAuthProvidersSourceSmoke, TestSocialHooksSmoke; README
Replaces TestTurnstileRoutesSmoke: empty adds nothing; open, closed and a
space-padded closed add turnstile:{mode} to POST login and register only,
every other route keeps its middleware; invalid values fail naming the key.
- empty (default) adds nothing; open or closed appends turnstile:{mode} to
POST /login and /register only; any other value fails boot naming the key
- TestTurnstileRoutesSmoke records the declared routes with a recording router
- TestRequestUser: bearer, token and auth_token cookies in guard order;
expired, foreign, blacklisted, revoked by tokens_valid_after and
deactivated tokens are anonymous; only infrastructure failures error
- TestEndRequestSession (no database): jti blacklisted until the token
expires, forget cookie with the configured Domain and Path, best effort
- TestAvatarURL: thumbnail, broken-image and storage-failure fallbacks,
gravatar with the size, the user payload's avatar_url unchanged
- TestDeactivatingEvent: one event per newly deactivated user inside the
bulk action's transaction, a listener error rolls the action back
- DeactivatingEvent and DeactivatingEventName in classes
- FireDeactivatingEvent modelled on FireDeletingEvent
- the deactivate bulk action runs DeactivateUsers unchanged, then fires
the event per newly deactivated user in the action's transaction
- AvatarURL returns the avatar thumbnail, the file path when the thumbnail
fails, or the gravatar URL, the rule of the user payload
- applyAvatar takes avatar_url from it; the payload is unchanged
- RequestUser resolves the signed-in user exactly like the jwt guard
(bearer, token and auth_token cookies, blacklist, tokens_valid_after)
and returns nil for anonymous or invalid requests
- EndRequestSession blacklists the session JWT and forgets the token
cookie with the configured cookie attributes, best effort
Register lighthouse user:{id} if the namespace is free, fire user.Deleting before unscoped delete, and backfill missing handles.
Co-authored-by: Cursor <cursoragent@cursor.com>
Generate a unique mention handle on API register and admin create, and fire user.Created from both paths so listeners run in the same write.
Co-authored-by: Cursor <cursoragent@cursor.com>
Hidden() already listed profile_data, but encoding/json would still emit
the Jsonable field. Tag it json:"-" so the DATA-07 marshal backstop holds.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Migration up/down covers jsonb profile_data, unique indexes, and cascade FK
- GET list with Encrypted tokens fails if plaintext, key names, or [redacted] leak
Co-authored-by: Cursor <cursoragent@cursor.com>
- Unlink 204 keeps the sibling row
- Last-method 409 matches EN and PL lang strings even with a password
- Missing, foreign, and unknown providers share byte-identical Winter HTML 404s
- Unauthenticated GET and DELETE /google return 401 JSON
Co-authored-by: Cursor <cursoragent@cursor.com>
- Destroy allow-lists providers and shares WriteNotFound for unknown, missing and foreign rows
- Last remaining identity is refused with a localized 409; password flags are unused
- EN and PL last_method_blocked strings match the PHP texts
- Model golem15_user_oauth_identities with Encrypted tokens and empty Fillable
- gormigrate 202610050001 with both unique indexes, cascade FK, and jsonb profile_data
- OAuthIdentitiesIndex returns {"data":[]} for a signed-in caller with no rows
The removal check for the json tag of users.last_seen showed that the
subtest read the user API payloads only, which are built field by field.
It now marshals the model as well, so a tag that would expose an admin
column fails it.
- resolver: several groups, numbers stored as text, empty group sets, a code only at user level, booleans
- migrations: every id once, the history applied twice, the unique code after a down and an up
- personal API tokens: no application, no principal, invalid bodies, another user's token
- each controller driven directly on records of another type, an application that is not booted, one without a database and one whose database fails: a guard that cannot read answers a plain error, never a pass
- D-30: a superuser passes the takeover guard; an unreadable membership is the opaque 500 with nothing changed
- unknown partials, undeclared filters and sorts, options of fields that have none, search and sort on the three lists
- a name-only update of a privileged group, validation before the guard, a seeded group protected through the config list
- known gaps pinned: boolean values in a group's permissions, organisation_role after an unlink
- classes: every admin action for its changed-row count and idempotency, the ban across several throttle rows, the cleanup of a permanent delete
- classes: the privileged list and IsPrivilegedMember (D-30), also through a handle that carries conditions
- classes: the five-minute boundary of last_seen
- models: PermissionSet round trip, the slug table shared with the SPA, Fillable and Rules, fields that never marshal, the group filter value
- TestAdminRegistration: permissions, navigation, embedded files and phrase keys in en and pl; the admin secret is required
User.FilterScope handed the filter value to the database as it came. A
value such as abc failed the query, so the Users list answered 500.
- groupFilterID reads a positive whole number, as a number or its text
- any other value narrows the list to nothing
- TestAdminUsersGroupFilterValue fails with a 500 without the change
classes.fresh chained WithContext onto a lazy NewDB session, which clones the
caller's statement with its conditions. A handle that carried a condition
made IsPrivilegedMember answer false for a member of a privileged group, so
the takeover guard would have let the request through. No caller passes such
a handle today: the admin hooks hand over the write's transaction handle.
- fresh builds the empty statement before anything is chained onto it
- IsPrivilegedMember and PrivilegedGroupIDs read through fresh
- one subtest per threat id T-12.1-18 to T-12.1-25, 27 to 31, 34, 38 and 39
- T-12.1-28 revisits T-12-18: the groups field is the one writer of users_groups
- T-12.1-38 and T-12.1-39 cover the takeover guard for privileged-group members
- controller golem15.user.organisations behind golem15.users.access_users
- list and form (name, slug preset from the name, description, avatar)
- members relation manager: link sets and unlink clears users.organisation_id
- deleting an organisation releases its members first
- Organisation gains Fillable, Rules, MorphName, AttachRelations, BeforeValidate
and the Members relation; models.Slugify
- navigation side item organisations
- TestAdminOrganisations, TestAdminOrganisationMembers, TestSlugify
- controller golem15.user.usergroups behind golem15.users.access_groups
- list with users_count from the list's own query; form with the checkbox
permission editor stored in user_groups.permissions
- creating a group with a privileged code, changing a code to or from one and
deleting a privileged group need golem15.users.manage_privileged_groups
- a group delete removes its users_groups rows in the same transaction
- UserGroup gains Fillable, Rules and the read-only UsersCount; PermissionSet.Text
- TestAdminGroups and the D-06 matrix in TestAdminPrivilegedGroups
- groups relation field on the user form, the only writer of users_groups
- UsersGroup pivot model
- AdminRelationLocks: adding or removing a group whose code is in
golem15.user.privileged_groups needs golem15.users.manage_privileged_groups,
on create and on update; refused saves answer 403 and write nothing
- TestAdminUserGroupsField and TestAdminPrivilegedGroups
One conditional UPDATE writes it at most once per five minutes per
user. A failed write is logged and never fails authentication, and no
user payload carries the column.
- password with confirmation on create, optional reset on update that
ends the user's sessions; the hash is never returned
- send_invite sends one invitation with an activation link after the
create commits
- organisation picker, avatar upload shared with the user API and the
frontend permission editor on the user form
- an email held by a deactivated user answers 422 instead of failing
on the unique constraint
- the privileged-member guard now also refuses a submitted password
- PermissionSet reads the stored JSON tolerantly and writes an object
of integers; FrontendPermission is the catalogue row
- MergedPermissions, HasPermission and UserHasPermission port PHP
getMergedPermissions and Winter's hasPermission: groups merge with
most positive wins, the user's own values override, held means
exactly 1, wildcards as in Winter
- User gains Permissions (never serialized) and declares the avatar
attachment relation the user API already writes
- the list opens the preview, which shows one status hint (banned,
deactivated, not activated, suspended) and the IP address fields
- bulk actions activate, deactivate, restore, ban and unban; record
actions activate, unban and unsuspend, offered while they apply
- delete (form button and bulk) removes the user for good with the
throttle rows, group memberships and attachments in one transaction;
blobs go after the commit
- a privileged-group member is still refused, a bulk delete as a whole
- admin harness: boots the plugin with the admin API mounted and
mints backend administrators with chosen permissions
- TestAdminUsersTracer and TestAdminPrivilegedMember
- permissions (the four Winter codes and manage_privileged_groups),
the user navigation item and the embedded admin YAML
- Users controller: list with deactivated users, row states, the
groups, created date and activated filters, and the form's name,
surname and email
- a member of a group in golem15.user.privileged_groups (default
admin) keeps their email, password and row unless the administrator
holds golem15.users.manage_privileged_groups; the refusal is a 403
that saves nothing
- users.permissions and users.last_seen as additive columns
- golem15_user_frontend_permissions (code, label, tab, comment)
- User gains LastSeen, read-only CreatedAt and UpdatedAt and the
filterByGroup list scope; none of them is serialized
- migration tests roll back by position past the three new steps
- RegisterUser: nil handle, validation refusals that write nothing, the
activation and IP options, the configured bcrypt cost, input untouched
- FireRegisterEvent: payload without password keys, listener error
returned, nil app and nil fields
- IssueToken: issuer from the request URL and X-Forwarded-Proto, nil
user, nil request and a missing JWT secret refused
- APIArray: listener merge, listener error, nil user
- RegisterEvent.Payload: a copy of the register input without password and
password_confirmation (Winter's golem15.user.register data array)
- RegisterUser, RegisterOptions, FireRegisterEvent, IssueToken and APIArray:
the steps POST /register runs, now called by Register itself; its status
codes and bodies are unchanged
- README documents the exports and Payload
- UserGroupCodes and HasGroupCode against Postgres: id order, code-less
groups skipped, membership by code, a missing handle and a failing query
reported as errors
- mail locale resolution, hook registration (a failing hook stops the
chain), the code lifetimes and throttle settings carried on the context,
constant-time code comparison, JWT secret loading and the principal
lookup without an app or a gorm handle
- user_groups (seeded with the guest and registered groups) and the
users_groups pivot, folded from the PHP v1.1.1 and v2.8.0 migrations
into migration 202610020001_create_user_groups
- models.UserGroup and a User.Groups relation that is never serialized
- classes.UserGroupCodes and classes.HasGroupCode for server-side checks
such as the site-admin group code admin
- additive only: no route, payload or existing column changes; the user
payload still answers "groups":[]