- /update clears email_verified_at in the same write when the email changes (case-only changes keep it)
- the admin Users form clears it in FormBeforeUpdate; storedEmail helper shared with guardCredentials
- README: the proof follows the address, never mass-assigned, no backfill
- additive migration 202610210001_add_users_email_verified_at (ADD COLUMN IF NOT EXISTS, no backfill, no-op rollback)
- User.EmailVerifiedAt (json:"-", not fillable)
- VerifyActivationCode and ResetPassword stamp it in the write that activates or stores the password
- 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
- 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
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>
- 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
- 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
- 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
- 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
- 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
Prefix replacement of the old application-nested module path in go.mod
and every import. No identifier, string, table, migration ID, config key
or comment changes.
Match PHP payloads, already-activated Winter 500 HTML, and direct-DB
seeding so the corpus can flip those routes to ported without onboarding.
Co-authored-by: Cursor <cursoragent@cursor.com>
Reset and activation codes expire on a TTL, and a missing issued-at still accepts a cutover code. Forgot-password always returns the same 200 body.
Co-authored-by: Cursor <cursoragent@cursor.com>
Registration follows activate_mode and hides a disabled or throttled cause in production. Fonoteka adds organisation fields through GetApiArrayEvent.
Co-authored-by: Cursor <cursoragent@cursor.com>
Wire the user API session loop to the JWT blacklist and a pre-password throttle gate so a logged-out token cannot be fetched again.
Co-authored-by: Cursor <cursoragent@cursor.com>