Skip to content

Commit 71d5049

Browse files
committed
feat: #98 bootstrap admin suite
1 parent 2bdf780 commit 71d5049

File tree

23 files changed

+664
-12
lines changed

23 files changed

+664
-12
lines changed

Diff for: _ide_helper_actions.php

+15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
<?php
22

3+
namespace App\Actions\Admin\Teams;
4+
5+
/**
6+
* @method static \Lorisleiva\Actions\Decorators\JobDecorator|\Lorisleiva\Actions\Decorators\UniqueJobDecorator makeJob(\App\Models\Team $team, \App\Models\QuotasOverride $quotasOverride)
7+
* @method static \Lorisleiva\Actions\Decorators\UniqueJobDecorator makeUniqueJob(\App\Models\Team $team, \App\Models\QuotasOverride $quotasOverride)
8+
* @method static \Illuminate\Foundation\Bus\PendingDispatch dispatch(\App\Models\Team $team, \App\Models\QuotasOverride $quotasOverride)
9+
* @method static \Illuminate\Foundation\Bus\PendingDispatch|\Illuminate\Support\Fluent dispatchIf(bool $boolean, \App\Models\Team $team, \App\Models\QuotasOverride $quotasOverride)
10+
* @method static \Illuminate\Foundation\Bus\PendingDispatch|\Illuminate\Support\Fluent dispatchUnless(bool $boolean, \App\Models\Team $team, \App\Models\QuotasOverride $quotasOverride)
11+
* @method static dispatchSync(\App\Models\Team $team, \App\Models\QuotasOverride $quotasOverride)
12+
* @method static dispatchNow(\App\Models\Team $team, \App\Models\QuotasOverride $quotasOverride)
13+
* @method static dispatchAfterResponse(\App\Models\Team $team, \App\Models\QuotasOverride $quotasOverride)
14+
* @method static mixed run(\App\Models\Team $team, \App\Models\QuotasOverride $quotasOverride)
15+
*/
16+
class OverrideTeamQuotas {}
17+
318
namespace App\Actions\Nodes;
419

520
/**

Diff for: app/Actions/Admin/Teams/OverrideTeamQuotas.php

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace App\Actions\Admin\Teams;
4+
5+
use App\Models\QuotasOverride;
6+
use App\Models\Team;
7+
use Illuminate\Http\Request;
8+
use Lorisleiva\Actions\Concerns\AsAction;
9+
10+
class OverrideTeamQuotas
11+
{
12+
use AsAction;
13+
14+
public function authorize(Request $request): bool
15+
{
16+
return $request->user()->isAdmin();
17+
}
18+
19+
public function rules(Request $request): array
20+
{
21+
return [
22+
'quotas' => QuotasOverride::getValidationRules($request->get('quotas')),
23+
];
24+
}
25+
26+
public function handle(Team $team, QuotasOverride $quotasOverride)
27+
{
28+
$team->quotas_override = $quotasOverride;
29+
30+
$team->save();
31+
}
32+
33+
public function asController(Team $team, Request $request)
34+
{
35+
$this->handle($team, QuotasOverride::from($request->get('quotas')));
36+
37+
return redirect()->route('admin.teams.show', $team->id)->with('message', 'Team quotas updated successfully.');
38+
}
39+
}

Diff for: app/Actions/Fortify/CreateNewUser.php

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Actions\Fortify;
44

5+
use App\Models\QuotasOverride;
56
use App\Models\Team;
67
use App\Models\User;
78
use Illuminate\Support\Facades\DB;
@@ -50,6 +51,7 @@ protected function createTeam(User $user): void
5051
'personal_team' => true,
5152
'billing_email' => $user->email,
5253
'billing_name' => $user->name,
54+
'quotas_override' => QuotasOverride::from([]),
5355
]));
5456
}
5557
}

Diff for: app/Actions/Jetstream/CreateTeam.php

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Actions\Jetstream;
44

5+
use App\Models\QuotasOverride;
56
use App\Models\Team;
67
use App\Models\User;
78
use Illuminate\Support\Facades\Gate;
@@ -34,6 +35,7 @@ public function create(User $user, array $input): Team
3435
'personal_team' => false,
3536
'billing_name' => $input['billing_name'],
3637
'billing_email' => $input['billing_email'],
38+
'quotas_override' => QuotasOverride::from([]),
3739
]));
3840

3941
return $team;

Diff for: app/Http/Controllers/Admin/TeamController.php

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Admin;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Models\Team;
7+
use Inertia\Inertia;
8+
9+
class TeamController extends Controller
10+
{
11+
public function index()
12+
{
13+
$teams = Team::with('owner')
14+
->withCount('nodes', 'services', 'onlineNodes', 'deployments')
15+
->paginate(30);
16+
17+
return Inertia::render('Admin/Teams/List', [
18+
'teams' => $teams,
19+
]);
20+
}
21+
22+
public function show(Team $team)
23+
{
24+
$team->load('owner');
25+
$quotas = $team->quotas();
26+
$plan = $team->currentPlan();
27+
28+
$quotaTypes = array_keys($plan->quotas);
29+
$quotasArray = [];
30+
31+
foreach ($quotaTypes as $type) {
32+
$quotasArray[$type] = [
33+
'maxUsage' => $quotas->$type->maxUsage,
34+
'currentUsage' => $quotas->$type->currentUsage(),
35+
'isSoft' => $plan->quotas[$type]['soft'],
36+
];
37+
}
38+
39+
return Inertia::render('Admin/Teams/Edit', [
40+
'team' => $team,
41+
'quotas' => $quotasArray,
42+
]);
43+
}
44+
}

Diff for: app/Http/Middleware/AdminAccess.php

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace App\Http\Middleware;
4+
5+
use Closure;
6+
use Illuminate\Http\Request;
7+
use Symfony\Component\HttpFoundation\Response;
8+
9+
class AdminAccess
10+
{
11+
/**
12+
* Handle an incoming request.
13+
*
14+
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
15+
*/
16+
public function handle(Request $request, Closure $next): Response
17+
{
18+
if (! $request->user() || ! $request->user()->isAdmin()) {
19+
abort(403, 'Unauthorized action.');
20+
}
21+
22+
return $next($request);
23+
}
24+
}

Diff for: app/Models/Node.php

+6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use App\Traits\HasOwningTeam;
66
use App\Util\AgentToken;
7+
use Illuminate\Database\Eloquent\Builder;
78
use Illuminate\Database\Eloquent\Factories\HasFactory;
89
use Illuminate\Database\Eloquent\Model;
910
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -60,6 +61,11 @@ public function tasks(): HasManyThrough
6061
return $this->hasManyThrough(NodeTask::class, NodeTaskGroup::class, 'node_id', 'task_group_id', 'id', 'id');
6162
}
6263

64+
public function scopeOnline(Builder $query): Builder
65+
{
66+
return $query->where('last_seen_at', '>', now()->subSeconds(35));
67+
}
68+
6369
public function getOnlineAttribute()
6470
{
6571
return $this->last_seen_at > now()->subSeconds(35);

Diff for: app/Models/QuotasOverride.php

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace App\Models;
4+
5+
use Spatie\LaravelData\Data;
6+
7+
class QuotasOverride extends Data
8+
{
9+
public function __construct(
10+
public int $nodes = 0,
11+
public int $swarms = 0,
12+
public int $services = 0,
13+
public int $deployments = 0
14+
) {}
15+
}

Diff for: app/Models/Team.php

+11-9
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ protected function casts(): array
5959
{
6060
return [
6161
'personal_team' => 'boolean',
62+
'quotas_override' => QuotasOverride::class,
6263
];
6364
}
6465

@@ -85,6 +86,11 @@ public function nodes(): HasMany
8586
return $this->hasMany(Node::class);
8687
}
8788

89+
public function onlineNodes(): HasMany
90+
{
91+
return $this->hasMany(Node::class)->online();
92+
}
93+
8894
public function services(): HasMany
8995
{
9096
return $this->hasMany(Service::class);
@@ -173,40 +179,36 @@ public function currentPlan(): Plan
173179
public function quotas(): UsageQuotas
174180
{
175181
$plan = $this->currentPlan();
182+
$override = $this->quotas_override;
176183

177184
return new UsageQuotas(
178185
new ItemQuota(
179186
name: 'Nodes',
180-
maxUsage: $plan->quotas['nodes']['limit'],
187+
maxUsage: max($plan->quotas['nodes']['limit'], $override->nodes),
181188
getCurrentUsage: fn () => $this->nodes()->count(),
182189
isSoftQuota: $plan->quotas['nodes']['soft']
183190
),
184191
new ItemQuota(
185192
name: 'Swarms',
186-
maxUsage: $plan->quotas['swarms']['limit'],
193+
maxUsage: max($plan->quotas['swarms']['limit'], $override->swarms),
187194
getCurrentUsage: fn () => $this->swarms()->count(),
188195
isSoftQuota: $plan->quotas['swarms']['soft']
189196
),
190197
new ItemQuota(
191198
name: 'Services',
192-
maxUsage: $plan->quotas['services']['limit'],
199+
maxUsage: max($plan->quotas['services']['limit'], $override->services),
193200
getCurrentUsage: fn () => $this->services()->count(),
194201
isSoftQuota: $plan->quotas['services']['soft']
195202
),
196203
new ItemQuota(
197204
name: 'Deployments',
198-
maxUsage: $plan->quotas['deployments']['limit'],
205+
maxUsage: max($plan->quotas['deployments']['limit'], $override->deployments),
199206
getCurrentUsage: fn () => $this->deployments()->count(),
200207
isSoftQuota: $plan->quotas['deployments']['soft']
201208
)
202209
);
203210
}
204211

205-
public function nodesLimitReached(): bool
206-
{
207-
return $this->nodes()->count() >= $this->quotas()->nodes;
208-
}
209-
210212
public function validSubscription(): ?Subscription
211213
{
212214
if (! config('billing.enabled')) {

Diff for: app/Models/User.php

+5
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,9 @@ protected function casts(): array
6464
'password' => 'hashed',
6565
];
6666
}
67+
68+
public function isAdmin(): bool
69+
{
70+
return in_array($this->email, config('auth.admin.emails'));
71+
}
6772
}

Diff for: bootstrap/app.php

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use ApiNodes\Http\Middleware\AgentTokenAuth;
44
use App\Console\Commands\DispatchProcessBackupTask;
55
use App\Console\Commands\DispatchVolumeBackupTask;
6+
use App\Http\Middleware\AdminAccess;
67
use App\Http\Middleware\HandleInertiaRequests;
78
use App\Jobs\CheckAgentUpdates;
89
use App\Models\Scopes\TeamScope;
@@ -41,6 +42,7 @@
4142
->alias([
4243
'abilities' => CheckAbilities::class,
4344
'ability' => CheckForAnyAbility::class,
45+
'admin' => AdminAccess::class,
4446
]);
4547
})
4648
->withExceptions(function (Exceptions $exceptions) {

Diff for: config/auth.php

+3
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,7 @@
112112

113113
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
114114

115+
'admin' => [
116+
'emails' => explode(',', env('ADMIN_EMAILS', '')),
117+
],
115118
];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
public function up(): void
10+
{
11+
Schema::table('teams', function (Blueprint $table) {
12+
$table->json('quotas_override')->default(json_encode([
13+
'nodes' => 0,
14+
'swarms' => 0,
15+
'services' => 0,
16+
'deployments' => 0,
17+
]));
18+
});
19+
}
20+
21+
public function down(): void
22+
{
23+
Schema::table('teams', function (Blueprint $table) {
24+
$table->dropColumn('quotas_override');
25+
});
26+
}
27+
};

Diff for: resources/js/Components/Admin/InfoField.vue

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<script setup>
2+
import { defineProps } from "vue";
3+
import InputLabel from "@/Components/InputLabel.vue";
4+
5+
const props = defineProps({
6+
label: String,
7+
value: String,
8+
});
9+
</script>
10+
11+
<template>
12+
<div class="bg-gray-50 rounded-lg p-4">
13+
<InputLabel :value="label" class="text-sm font-medium text-gray-500" />
14+
<div class="mt-1 text-lg font-semibold text-gray-900">{{ value }}</div>
15+
</div>
16+
</template>

Diff for: resources/js/Components/Card.vue

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<template>
2+
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-6">
3+
<slot></slot>
4+
</div>
5+
</template>

0 commit comments

Comments
 (0)