Skip to content

fix: repair four defects in the never-executed application layer - #11

Merged
WAHIB-EL-KHADIRI merged 1 commit into
mainfrom
fix/application-layer-defects
Aug 25, 2026
Merged

WAHIB-EL-KHADIRI merged 1 commit into
mainfrom
fix/application-layer-defects

Conversation

@WAHIB-EL-KHADIRI

Copy link
Copy Markdown
Owner

Fixes 1–4 of #10.

These use cases are written, complete-looking, and none of them works. Nothing
has ever called them, so nothing has ever found out.

1. Membership changes now require an authorized actor

AddTeamMemberUseCase and AddProjectMemberUseCase checked that the target
was not already a member, and never considered who was asking.

AuthMiddleware answers is this someone, not may this someone change this
membership
. Wired as they stood, any logged-in user could have added any
account — including their own, at role = 'admin' — to any team or project.

Both now take a required $actorId and refuse unless that user is already an
admin of the group. It is deliberately not defaulted: a default would let
a caller omit an authorization decision without noticing.

2. CreateTeamUseCase could not insert a row

It wrote created_by. teams has owner_id INT UNSIGNED NOT NULL and no
created_by column, and Database::insert() builds its column list straight
from the array keys — so MySQL rejected the statement outright.

3. CreateWorkspaceUseCase failed three ways at once

Same wrong column, plus owner_id NOT NULL and
slug VARCHAR(200) NOT NULL UNIQUE were never set, and nothing in the
codebase generated a slug.

It now sets the owner and derives a slug from the name with a random suffix.
The column is unique, so a collision is a failed insert rather than a cosmetic
problem — and for a value that is not user-facing, a random suffix is cheaper
and more predictable than a query-and-retry loop.

4. A new workspace was invisible to the person who created it

CreateTeamUseCase and CreateProjectUseCase both call
addMember(..., 'admin') after inserting. This one did not, and
UserRepository::getWorkspaces() reads through workspace_members:

JOIN workspace_members wm ON w.id = wm.workspace_id WHERE wm.user_id = ?

So even with 2 and 3 fixed, the creator would not have seen it on their own
dashboard. WorkspaceRepository::addMember() already existed.

Not fixed here

Item 5 of #10WorkspaceController::update/addMember/removeMember have no
application layer at all. That needs decisions about who may change a
workspace, not repair.

Safety

The two signature changes are safe: these use cases still have no callers, so
nothing can be silently passing the old argument list.

Not verified locally — no PHP on this machine. phpcs and phpstan level 5
run here; behaviour stays uncovered until #2.

🤖 Generated with Claude Code

These use cases are written and complete-looking, and none of them works.
Nothing has ever called them, so nothing has ever found out.

**Membership changes now require an authorized actor.**
AddTeamMemberUseCase and AddProjectMemberUseCase checked that the target was
not already a member and never considered who was asking. The routes carry
AuthMiddleware, which answers "is this someone", not "may this someone change
this membership" -- so once wired, any logged-in user could have added any
account, at role 'admin', to any team or project. Both now take a required
$actorId and refuse unless that user is already an admin of the group.
$actorId is deliberately not defaulted: a default would let a caller omit an
authorization decision without noticing.

**CreateTeamUseCase could not insert a row.** It wrote `created_by`. `teams`
has `owner_id NOT NULL` and no `created_by` column, and Database::insert()
builds its column list straight from the array keys, so MySQL rejected the
statement.

**CreateWorkspaceUseCase failed three ways.** Same wrong column, plus
`owner_id NOT NULL` and `slug VARCHAR(200) NOT NULL UNIQUE` were never set and
nothing generated a slug. It now sets the owner and derives a slug from the
name with a random suffix -- the column is unique, so a collision is a failed
insert, and a random suffix is cheaper and more predictable than a retry loop
for a value that is not user-facing.

**Creating a workspace left it invisible to its creator.** CreateTeamUseCase
and CreateProjectUseCase both call addMember(..., 'admin') after inserting;
this one did not, and UserRepository::getWorkspaces() reads through
workspace_members. WorkspaceRepository::addMember() already existed.

Reported with evidence in #10. The one remaining item there is the three
workspace methods with no application layer at all, which needs decisions
rather than repair.

The signature changes are safe: these use cases still have no callers.

Not verified locally: no PHP on this machine. phpcs and phpstan run in CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@WAHIB-EL-KHADIRI
WAHIB-EL-KHADIRI merged commit 1372f31 into main Aug 25, 2026
1 check passed
@WAHIB-EL-KHADIRI
WAHIB-EL-KHADIRI deleted the fix/application-layer-defects branch August 25, 2026 17:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant