fix(coolify-access): prefer managed root team
All checks were successful
Build MapleDeploy Coolify Image / build (push) Successful in 41s
All checks were successful
Build MapleDeploy Coolify Image / build (push) Successful in 41s
This commit is contained in:
parent
e3cb2675dd
commit
65d85fb890
8 changed files with 166 additions and 11 deletions
|
|
@ -103,6 +103,7 @@ private function createMember(array $input): int
|
|||
'password' => Hash::make($input['password']),
|
||||
]);
|
||||
$user->markEmailAsVerified();
|
||||
$this->deletePersonalTeams($user);
|
||||
$user->teams()->syncWithoutDetaching([
|
||||
$rootTeam->id => ['role' => $input['team_role']],
|
||||
]);
|
||||
|
|
@ -113,6 +114,25 @@ private function createMember(array $input): int
|
|||
return $this->succeedWithUser($user);
|
||||
}
|
||||
|
||||
private function deletePersonalTeams(User $user): void
|
||||
{
|
||||
// MapleDeploy branding: dashboard-managed users should only see the
|
||||
// managed instance root team, not an empty personal Coolify team.
|
||||
$personalTeams = Team::query()
|
||||
->where('teams.id', '!=', 0)
|
||||
->where('personal_team', true)
|
||||
->whereHas('members', fn ($query) => $query->whereKey($user->id))
|
||||
->get();
|
||||
|
||||
foreach ($personalTeams as $team) {
|
||||
DB::table('team_user')
|
||||
->where('team_id', $team->id)
|
||||
->where('user_id', $user->id)
|
||||
->delete();
|
||||
DB::table('teams')->where('id', $team->id)->delete();
|
||||
}
|
||||
}
|
||||
|
||||
private function readPassword(): string
|
||||
{
|
||||
return rtrim((string) stream_get_contents(STDIN), "\n");
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace App\Console\Commands\Mapledeploy;
|
||||
|
||||
use App\Enums\Role;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
|
@ -42,6 +44,13 @@ public function handle(): int
|
|||
if (! $user) {
|
||||
return $this->failWith('USER_NOT_FOUND');
|
||||
}
|
||||
$rootTeam = null;
|
||||
if ((int) $user->id !== 0) {
|
||||
$rootTeam = Team::find(0);
|
||||
if (! $rootTeam) {
|
||||
return $this->failWith('ROOT_TEAM_MISSING');
|
||||
}
|
||||
}
|
||||
|
||||
$changes = [
|
||||
'password' => Hash::make($password),
|
||||
|
|
@ -60,13 +69,23 @@ public function handle(): int
|
|||
$changes['name'] = $input['name'];
|
||||
}
|
||||
|
||||
$user->forceFill($changes)->save();
|
||||
if ($updatesOwner && ! $user->hasVerifiedEmail()) {
|
||||
$user->markEmailAsVerified();
|
||||
}
|
||||
// MapleDeploy branding: password resets from the dashboard should end
|
||||
// any browser sessions authenticated with the previous password.
|
||||
DB::table('sessions')->where('user_id', $user->id)->delete();
|
||||
DB::transaction(function () use ($user, $changes, $updatesOwner, $rootTeam) {
|
||||
$user->forceFill($changes)->save();
|
||||
if ($updatesOwner && ! $user->hasVerifiedEmail()) {
|
||||
$user->markEmailAsVerified();
|
||||
}
|
||||
if ($rootTeam) {
|
||||
// MapleDeploy branding: matching an existing Coolify user by
|
||||
// email must grant the same root-team admin access as a newly
|
||||
// dashboard-created user.
|
||||
$user->teams()->syncWithoutDetaching([
|
||||
$rootTeam->id => ['role' => Role::ADMIN->value],
|
||||
]);
|
||||
}
|
||||
// MapleDeploy branding: password resets from the dashboard should
|
||||
// end browser sessions authenticated with the previous password.
|
||||
DB::table('sessions')->where('user_id', $user->id)->delete();
|
||||
});
|
||||
|
||||
$this->line(json_encode([
|
||||
'user' => [
|
||||
|
|
|
|||
|
|
@ -117,7 +117,9 @@ public function link()
|
|||
$user->teams()->attach($team->id, ['role' => $invitation->first()->role]);
|
||||
$invitation->delete();
|
||||
} else {
|
||||
$team = $user->teams()->first();
|
||||
// MapleDeploy branding: root-team admins should land in
|
||||
// the managed instance team, not their empty personal team.
|
||||
$team = $user->mapledeployPreferredTeam();
|
||||
}
|
||||
Auth::login($user);
|
||||
session(['currentTeam' => $team]);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,16 @@ public function handle(Request $request, Closure $next): Response
|
|||
$currentTeam = auth()->user()?->recreate_personal_team();
|
||||
refreshSession($currentTeam);
|
||||
}
|
||||
$preferredTeam = auth()?->user()?->mapledeployPreferredTeam();
|
||||
if (
|
||||
$preferredTeam &&
|
||||
$preferredTeam->id === 0 &&
|
||||
auth()?->user()?->currentTeam()?->id !== 0
|
||||
) {
|
||||
// MapleDeploy branding: repair sessions that landed in the empty
|
||||
// personal team before dashboard-managed root-team access existed.
|
||||
refreshSession($preferredTeam);
|
||||
}
|
||||
if (auth()?->user()?->currentTeam()) {
|
||||
refreshSession(auth()->user()->currentTeam());
|
||||
} elseif (auth()?->user()?->teams?->count() > 0) {
|
||||
|
|
|
|||
|
|
@ -209,6 +209,20 @@ public function recreate_personal_team()
|
|||
return $new_team;
|
||||
}
|
||||
|
||||
public function mapledeployPreferredTeam(): ?Team
|
||||
{
|
||||
// MapleDeploy branding: dashboard-managed users are attached to the
|
||||
// root team so they can administer the customer's managed instance.
|
||||
$rootTeam = $this->teams->firstWhere('id', 0);
|
||||
$rootRole = data_get($rootTeam, 'pivot.role');
|
||||
if ($rootTeam && ($rootRole === 'admin' || $rootRole === 'owner')) {
|
||||
return $rootTeam;
|
||||
}
|
||||
|
||||
return $this->teams->firstWhere('personal_team', true)
|
||||
?? $this->teams->first();
|
||||
}
|
||||
|
||||
public function createToken(string $name, array $abilities = ['*'], ?DateTimeInterface $expiresAt = null)
|
||||
{
|
||||
$plainTextToken = sprintf(
|
||||
|
|
|
|||
|
|
@ -96,8 +96,9 @@ public function boot(): void
|
|||
$user->currentTeam = $invitation->team;
|
||||
$invitation->delete();
|
||||
} else {
|
||||
// Normal login - use personal team
|
||||
$user->currentTeam = $user->teams->firstWhere('personal_team', true);
|
||||
// MapleDeploy branding: root-team admins should land in
|
||||
// the managed instance team, not their empty personal team.
|
||||
$user->currentTeam = $user->mapledeployPreferredTeam();
|
||||
if (! $user->currentTeam) {
|
||||
$user->currentTeam = $user->recreate_personal_team();
|
||||
}
|
||||
|
|
|
|||
65
tests/Feature/MapledeployPreferredTeamLoginTest.php
Normal file
65
tests/Feature/MapledeployPreferredTeamLoginTest.php
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Middleware\DecideWhatToDoWithUser;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
config([
|
||||
'app.maintenance.store' => 'array',
|
||||
'cache.default' => 'array',
|
||||
]);
|
||||
});
|
||||
|
||||
test('MapleDeploy root team admins log in to the managed root team', function () {
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::query()->create(['id' => 0]));
|
||||
$rootTeam = Team::factory()->create([
|
||||
'id' => 0,
|
||||
'name' => 'Root Team',
|
||||
'personal_team' => false,
|
||||
]);
|
||||
$user = User::factory()->create([
|
||||
'email' => 'member@example.com',
|
||||
]);
|
||||
expect($user->teams()->where('personal_team', true)->exists())->toBeTrue();
|
||||
$user->teams()->syncWithoutDetaching([
|
||||
$rootTeam->id => ['role' => 'admin'],
|
||||
]);
|
||||
|
||||
$response = $this->post('/login', [
|
||||
'email' => 'member@example.com',
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
expect(session('currentTeam')?->id)->toBe(0);
|
||||
});
|
||||
|
||||
test('MapleDeploy root team admin sessions are repaired from personal team to root team', function () {
|
||||
$rootTeam = Team::factory()->create([
|
||||
'id' => 0,
|
||||
'name' => 'Root Team',
|
||||
'personal_team' => false,
|
||||
]);
|
||||
$user = User::factory()->create([
|
||||
'email' => 'member@example.com',
|
||||
]);
|
||||
$personalTeam = $user->teams()->where('personal_team', true)->firstOrFail();
|
||||
$user->teams()->syncWithoutDetaching([
|
||||
$rootTeam->id => ['role' => 'admin'],
|
||||
]);
|
||||
$this->actingAs($user);
|
||||
session(['currentTeam' => $personalTeam]);
|
||||
|
||||
app(DecideWhatToDoWithUser::class)->handle(
|
||||
Request::create('/'),
|
||||
fn () => response('ok'),
|
||||
);
|
||||
|
||||
expect(session('currentTeam')?->id)->toBe(0);
|
||||
});
|
||||
|
|
@ -145,7 +145,8 @@ function runMapledeployUserCommand(array $arguments, string $stdin = ''): array
|
|||
->and($member['json']['user']['email'])->toBe('member@example.com');
|
||||
|
||||
$memberUser = User::whereEmail('member@example.com')->firstOrFail();
|
||||
expect($memberUser->teams()->where('teams.id', 0)->first()?->pivot?->role)->toBe('admin');
|
||||
expect($memberUser->teams()->where('teams.id', 0)->first()?->pivot?->role)->toBe('admin')
|
||||
->and($memberUser->teams()->pluck('teams.id')->all())->toBe([0]);
|
||||
|
||||
$list = runMapledeployUserCommand(['mapledeploy:user:list']);
|
||||
expect($list['exitCode'])->toBe(0)
|
||||
|
|
@ -276,6 +277,29 @@ function runMapledeployUserCommand(array $arguments, string $stdin = ''): array
|
|||
expect(DB::table('sessions')->where('user_id', 0)->count())->toBe(0);
|
||||
});
|
||||
|
||||
test('MapleDeploy password command promotes matched native users to root team admin', function () {
|
||||
runMapledeployUserCommand([
|
||||
'mapledeploy:user:create',
|
||||
'--admin',
|
||||
'--email=owner@example.com',
|
||||
'--name=Owner',
|
||||
], "owner-password\n");
|
||||
$nativeUser = User::factory()->create([
|
||||
'email' => 'native-member@example.com',
|
||||
'name' => 'Native Member',
|
||||
]);
|
||||
expect($nativeUser->teams()->where('teams.id', 0)->exists())->toBeFalse();
|
||||
|
||||
$reset = runMapledeployUserCommand([
|
||||
'mapledeploy:user:set-password',
|
||||
(string) $nativeUser->id,
|
||||
], "native-member-password\n");
|
||||
|
||||
expect($reset['exitCode'])->toBe(0)
|
||||
->and(Hash::check('native-member-password', $nativeUser->fresh()->password))->toBeTrue()
|
||||
->and($nativeUser->fresh()->teams()->where('teams.id', 0)->first()?->pivot?->role)->toBe('admin');
|
||||
});
|
||||
|
||||
test('MapleDeploy password command rejects ownership transfer to an existing email', function () {
|
||||
runMapledeployUserCommand([
|
||||
'mapledeploy:user:create',
|
||||
|
|
|
|||
Loading…
Reference in a new issue