Commit Graph

56 Commits

Author SHA1 Message Date
Jakub Zych
f0d3b8b21d test(14.1-02): prove oauth-identities migration and secret-leak list
- 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>
2026-10-05 21:34:00 +02:00
Jakub Zych
026848fc02 test(14.1-02): port OAuthIdentityApiTest 204, 409, Winter 404s, 401
- 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>
2026-10-05 21:31:50 +02:00
Jakub Zych
ac8aa16217 docs(14.1-01): document host-mounted OAuth identity constructors
The table, migration and Index/Destroy constructors are a shared-plugin
contract; the host chooses the path and 404 writer.
2026-10-05 21:24:04 +02:00
Jakub Zych
8e172c5087 feat(14.1-01): unlink OAuth identities with 204 and last-method 409
- 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
2026-10-05 21:11:24 +02:00
Jakub Zych
63ac504c42 feat(14.1-01): add OAuth identities table and empty-list Index
- 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
2026-10-05 21:09:28 +02:00
Jakub Zych
2b04cda3ff test: T-12.1-24 also marshals a user with last_seen and permissions
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.
2026-10-05 15:46:52 +02:00
Jakub Zych
7991b53979 test: extend the permission resolver table, the admin migration history and the API token refusals
- 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
2026-10-05 15:23:23 +02:00
Jakub Zych
a848aa17d6 test: edge cases of the Users, User Groups and Organisations admin controllers
- 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
2026-10-05 15:23:23 +02:00
Jakub Zych
493a3ff47c test: unit tests for the admin class functions, the models and the admin registration
- 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
2026-10-05 15:15:28 +02:00
Jakub Zych
43fabfa6c3 fix: a group filter value that is not a group id lists nobody (D-23)
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
2026-10-05 15:15:28 +02:00
Jakub Zych
f4969591e2 fix: read privileged membership and the admin actions through a clean statement (T-12.1-38, T-12.1-39)
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
2026-10-05 14:53:41 +02:00
Jakub Zych
ba43ad9c03 test: pin every mitigated Phase 12.1 threat of the admin screens in TestPhase121Threats
- 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
2026-10-05 14:30:31 +02:00
Jakub Zych
4693155108 docs: describe the User Groups and Organisations screens and the privileged-group rules 2026-10-05 13:58:54 +02:00
Jakub Zych
58595fef27 feat: Organisations admin screen with the avatar and the members manager
- 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
2026-10-05 13:58:54 +02:00
Jakub Zych
a3992879db feat: User Groups admin screen with the users count and privileged codes protected
- 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
2026-10-05 13:52:26 +02:00
Jakub Zych
9611708792 feat: manage a user's groups from the user form, privileged groups locked
- 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
2026-10-05 13:44:15 +02:00
Jakub Zych
5805c6347f docs: describe the Users admin screen, its permissions and the privileged-member rule 2026-10-05 13:26:53 +02:00
Jakub Zych
c7d9d7d25d feat: stamp users.last_seen on login and token refresh
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.
2026-10-05 13:26:53 +02:00
Jakub Zych
f7ca3c4f59 feat: create users with a password and an invitation in the admin
- 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
2026-10-05 13:21:51 +02:00
Jakub Zych
cdea47a178 feat: frontend permissions and the merged-permission resolver
- 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
2026-10-05 13:21:51 +02:00
Jakub Zych
970eba7734 feat: user preview, bulk and record actions and the permanent delete
- 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
2026-10-05 13:10:12 +02:00
Jakub Zych
ebaf3a0817 refactor: spell the five permission codes out in the catalog
The catalog reads like the Winter registerPermissions() list, and a
test pins the codes and their default roles.
2026-10-05 13:03:36 +02:00
Jakub Zych
f3f33b809a style: gofmt the user model and the session test 2026-10-05 13:02:23 +02:00
Jakub Zych
4e17c0d35b test: cover the Users admin list, update and the privileged-member guard
- admin harness: boots the plugin with the admin API mounted and
  mints backend administrators with chosen permissions
- TestAdminUsersTracer and TestAdminPrivilegedMember
2026-10-05 13:01:59 +02:00
Jakub Zych
b410a7fea9 feat: register the Users admin screen with the privileged-member guard
- 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
2026-10-05 13:01:48 +02:00
Jakub Zych
e22f7660f3 feat: add the admin columns and the frontend permissions table
- 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
2026-10-05 12:51:41 +02:00
Jakub Zych
0fe5b91b63 feat: own shared user API tokens 2026-10-04 22:30:20 +02:00
Jakub Zych
8a658903ed test: cover the exported registration 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
2026-10-03 11:17:32 +02:00
Jakub Zych
d80d7990f6 feat: carry the register input on RegisterEvent and export the registration steps
- 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
2026-10-03 07:12:00 +02:00
Jakub Zych
c258e9fade test: cover user group codes and the small class helpers
- 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
2026-10-02 16:19:06 +02:00
Jakub Zych
86970074b7 feat: add Winter user groups
- 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":[]
2026-10-02 11:44:01 +02:00
Jakub Zych
c65ab3cdac build: require golang.org/x/image v0.46.0
The framework now registers the x/image webp decoder; tidy the module so
its go.sum matches the workspace build.
2026-10-02 11:35:06 +02:00
Jakub Zych
44f3c4c986 docs: describe the user plugin 2026-10-02 10:57:49 +02:00
Jakub Zych
1f2ea57a97 build: tidy go.mod for standalone module mode
GOWORK=off go mod tidy records the framework's transitive tidwall/*
requirements as indirect and adds the missing go.sum lines. No direct
requirement changes.
2026-10-02 10:56:12 +02:00
Jakub Zych
02ba1f0666 refactor: rename module to git.golem15.com/golem15/sm-user-plugin
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.
2026-10-02 10:56:06 +02:00
Jakub Zych
2004678506 chore(11-01): pick up the framework job queue tables and River dependency
- go mod tidy in the app and plugin modules for River v0.47.0
- allow-list summer_jobs and the River v7 tables in the schema diff
- expect summer_migrations_summercms_conga among the history tables
2026-09-29 15:02:08 +02:00
Jakub Zych
af976e789f refactor(10.2-01): use nested framework modules
- Rewrite application and plugin framework imports
2026-09-28 02:21:38 +02:00
Jakub Zych
96bf78845d refactor(10.2-01): use nested festival module
- Point user event test at modules/festival
2026-09-28 02:16:45 +02:00
Jakub Zych
7cdcb7cf89 test(08-07): add failing RED anchor for fonoteka:oauth-client command
- TestPhase8RedOAuthClientCommand drives the not-implemented command
  stub's create path and fails with PHASE8_RED:oauth-command until the
  real create/update/list behavior lands (D-19)
- oauth_client.go stub declares the exact PHP-parity flag/argument
  contract (name, --redirect-uri=*, --scope=*, --auth-method,
  --client-id, --list) so the RED test exercises the real command shape
- user/console_test.go's emailArg gains Flags(string) []string: bonfire's
  Input interface (summercms.go) grew this method for D-19's repeatable
  flags, and emailArg must satisfy it to keep building
2026-09-23 21:54:54 +02:00
Jakub Zych
d4066a8c3f test(07-07): cover already-activated activate and 22-ported corpus counts
Unit tests now expect Winter 500 HTML when the target user is already
active, and the inventory assertions match 169/22/147.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-22 23:57:10 +02:00
Jakub Zych
d94634d5d8 feat(07-07): replay the 15 user-api routes and both nuxt auth flows
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>
2026-09-22 23:57:00 +02:00
Jakub Zych
d5ec97c6f3 feat(07-06): cover the session sequence and each token scope
Register through logout is one in-process flow, and token mint accepts read, write, ai, and the two-scope combination.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-22 19:20:13 +02:00
Jakub Zych
63d6d049b7 feat(07-03): add avatars, mail templates, and password-change command
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-22 16:43:44 +02:00
Jakub Zych
5c1f857d54 feat(07-03): update profile, change password, and marketing consent
Changing a password invalidates older tokens and leaves the token that made the call signed in.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-22 16:32:05 +02:00
Jakub Zych
cb50d673c6 feat(07-03): add password reset and account activation
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>
2026-09-22 16:30:22 +02:00
Jakub Zych
97a39dc34c feat(07-02): register accounts and extend the user payload
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>
2026-09-22 15:11:14 +02:00
Jakub Zych
6bceb8167c feat(07-02): serve login, logout, fetch, and refresh
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>
2026-09-22 14:40:18 +02:00
Jakub Zych
00e6c51abd feat(07-02): throttle failed logins and pick mail template locale
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-22 14:03:31 +02:00
Jakub Zych
1311bd6cad feat(07-02): add user session schema, throttle table, and jwt config
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-22 13:53:16 +02:00
Jakub Zych
79e8373560 test(07-02): add failing tests for user session migrations
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-22 13:48:51 +02:00