Strip hosting Team & Developers pages; point to central account hub
Deploy Ladill Hosting / deploy (push) Successful in 50s
Deploy Ladill Hosting / deploy (push) Successful in 50s
Team and developer-token management now live on account.ladill.com. Remove the
local account.team* and account.developers* routes, the DeveloperController, and
the (empty) hosting Team/Developers pages; repoint the sidebar Team link to
ladill_account_url('/account/team'). The acting-account switcher (account.switch)
stays — it is load-bearing for multi-account access. Hosting developer-access
authz is already centralized via the identity API (shadow-verified + flipped).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
107013bd04
commit
f206f9bef5
@@ -1,45 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Hosting;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* Developers — personal API tokens for the Ladill Email API (Sanctum). Tokens
|
||||
* belong to the signed-in user and authorize calls to /api/v1/* (read-only for
|
||||
* now: account + mailbox listing). The plaintext token is shown once on create.
|
||||
*/
|
||||
class DeveloperController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
return view('hosting.account.developers', [
|
||||
'tokens' => $request->user()->tokens()->latest()->get(),
|
||||
'apiBase' => rtrim((string) config('app.url'), '/').'/api/v1',
|
||||
'newToken' => session('new_token'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:60'],
|
||||
]);
|
||||
|
||||
$token = $request->user()->createToken($data['name'], ['mailboxes:read']);
|
||||
|
||||
return redirect()->route('account.developers')
|
||||
->with('new_token', $token->plainTextToken)
|
||||
->with('success', 'Token created — copy it now, it won’t be shown again.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request, int $token): RedirectResponse
|
||||
{
|
||||
$request->user()->tokens()->whereKey($token)->delete();
|
||||
|
||||
return redirect()->route('account.developers')->with('success', 'Token revoked.');
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
<x-app-layout title="Developers — Ladill Hosting">
|
||||
<div class="mx-auto max-w-3xl">
|
||||
<h1 class="text-xl font-semibold tracking-tight text-slate-900">Developers</h1>
|
||||
<p class="mt-0.5 text-sm text-slate-500">API tokens to manage your hosting programmatically.</p>
|
||||
|
||||
@if($newToken)
|
||||
<div class="mt-6 rounded-2xl border border-emerald-200 bg-emerald-50 p-5">
|
||||
<p class="text-sm font-semibold text-emerald-900">Your new token — copy it now</p>
|
||||
<p class="mt-1 text-xs text-emerald-700">This is the only time it will be shown.</p>
|
||||
<div class="mt-3 flex items-center gap-2" x-data="{ copied: false }">
|
||||
<code class="flex-1 truncate rounded-lg bg-white px-3 py-2 font-mono text-xs text-slate-800 ring-1 ring-emerald-200">{{ $newToken }}</code>
|
||||
<button @click="navigator.clipboard.writeText('{{ $newToken }}'); copied = true; setTimeout(() => copied = false, 1500)"
|
||||
class="rounded-lg bg-emerald-600 px-3 py-2 text-xs font-semibold text-white hover:bg-emerald-700">
|
||||
<span x-show="!copied">Copy</span><span x-show="copied" x-cloak>Copied ✓</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mt-6 rounded-2xl border border-slate-200 bg-white p-5">
|
||||
<h2 class="text-sm font-semibold text-slate-900">Create a token</h2>
|
||||
<form method="POST" action="{{ route('account.developers.store') }}" class="mt-4 flex flex-col gap-3 sm:flex-row sm:items-end">
|
||||
@csrf
|
||||
<div class="flex-1">
|
||||
<label class="block text-[11px] font-medium text-slate-500">Token name</label>
|
||||
<input type="text" name="name" required placeholder="e.g. CI server"
|
||||
class="mt-1 w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
|
||||
@error('name')<p class="mt-1 text-xs text-rose-600">{{ $message }}</p>@enderror
|
||||
</div>
|
||||
<button class="btn-primary">Generate token</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 rounded-2xl border border-slate-200 bg-white">
|
||||
<div class="border-b border-slate-100 px-5 py-3.5"><h2 class="text-sm font-semibold text-slate-900">Your tokens</h2></div>
|
||||
@forelse($tokens as $token)
|
||||
<div class="flex items-center justify-between px-5 py-3.5 {{ ! $loop->last ? 'border-b border-slate-50' : '' }}">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-slate-900">{{ $token->name }}</p>
|
||||
<p class="text-xs text-slate-400">
|
||||
Created {{ $token->created_at->diffForHumans() }} ·
|
||||
{{ $token->last_used_at ? 'last used '.$token->last_used_at->diffForHumans() : 'never used' }}
|
||||
</p>
|
||||
</div>
|
||||
<x-confirm-dialog
|
||||
:name="'revoke-token-'.$token->id"
|
||||
title="Revoke API token?"
|
||||
:message="'Revoke '.$token->name.'? Apps using this token will stop working.'"
|
||||
:action="route('account.developers.destroy', $token->id)"
|
||||
method="DELETE"
|
||||
confirm-label="Revoke"
|
||||
>
|
||||
<x-slot:trigger>
|
||||
<button type="button" class="text-xs font-medium text-rose-600 hover:underline">Revoke</button>
|
||||
</x-slot:trigger>
|
||||
</x-confirm-dialog>
|
||||
</div>
|
||||
@empty
|
||||
<p class="px-5 py-8 text-center text-sm text-slate-400">No tokens yet.</p>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<div class="mt-6 rounded-2xl border border-slate-200 bg-slate-900 p-5 text-slate-200">
|
||||
<h2 class="text-sm font-semibold text-white">Quick start</h2>
|
||||
<p class="mt-1 text-xs text-slate-400">Authenticate with a Bearer token. Base URL:</p>
|
||||
<code class="mt-2 block rounded-lg bg-black/40 px-3 py-2 font-mono text-[11px] text-emerald-300">{{ $apiBase }}</code>
|
||||
<pre class="mt-3 overflow-x-auto rounded-lg bg-black/40 px-3 py-3 font-mono text-[11px] leading-relaxed text-slate-300"><code>curl {{ $apiBase }}/me \
|
||||
-H "Authorization: Bearer <your-token>" \
|
||||
-H "Accept: application/json"</code></pre>
|
||||
<p class="mt-3 text-[11px] text-slate-400">Endpoints: <span class="font-mono text-slate-300">GET /me</span>. More coming soon.</p>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -1,88 +0,0 @@
|
||||
<x-app-layout title="Team — Ladill Hosting">
|
||||
<div class="mx-auto max-w-3xl">
|
||||
<h1 class="text-xl font-semibold tracking-tight text-slate-900">Team</h1>
|
||||
<p class="mt-0.5 text-sm text-slate-500">Invite people to help manage this account’s hosting.</p>
|
||||
|
||||
@if($canManage)
|
||||
<div class="mt-6 rounded-2xl border border-slate-200 bg-white p-5">
|
||||
<h2 class="text-sm font-semibold text-slate-900">Invite a teammate</h2>
|
||||
<form method="POST" action="{{ route('account.team.store') }}" class="mt-4 flex flex-col gap-3 sm:flex-row sm:items-end">
|
||||
@csrf
|
||||
<div class="flex-1">
|
||||
<label class="block text-[11px] font-medium text-slate-500">Email</label>
|
||||
<input type="email" name="email" required placeholder="teammate@example.com"
|
||||
class="mt-1 w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
|
||||
@error('email')<p class="mt-1 text-xs text-rose-600">{{ $message }}</p>@enderror
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-medium text-slate-500">Role</label>
|
||||
<select name="role" class="mt-1 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none">
|
||||
<option value="member">Member</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn-primary">Send invite</button>
|
||||
</form>
|
||||
<p class="mt-2 text-[11px] text-slate-400">Admins can manage hosting and the team. Members can manage hosting. Invitees join by signing in with that email.</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mt-6 rounded-2xl border border-slate-200 bg-white">
|
||||
<div class="border-b border-slate-100 px-5 py-3.5"><h2 class="text-sm font-semibold text-slate-900">Members</h2></div>
|
||||
<ul class="divide-y divide-slate-50">
|
||||
<li class="flex items-center justify-between px-5 py-3.5">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="inline-flex h-9 w-9 items-center justify-center rounded-full bg-slate-900 text-xs font-semibold text-white">{{ strtoupper(substr($account->name ?? $account->email, 0, 1)) }}</span>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-slate-900">{{ $account->name ?? $account->email }} <span class="text-xs font-normal text-slate-400">(you)</span></p>
|
||||
<p class="text-xs text-slate-400">{{ $account->email }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="rounded-full bg-indigo-50 px-2.5 py-1 text-[11px] font-medium text-indigo-700">Owner</span>
|
||||
</li>
|
||||
|
||||
@forelse($members as $member)
|
||||
<li class="flex items-center justify-between px-5 py-3.5">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="inline-flex h-9 w-9 items-center justify-center rounded-full bg-slate-100 text-xs font-semibold text-slate-600">{{ strtoupper(substr($member->email, 0, 1)) }}</span>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-slate-900">{{ $member->member->name ?? $member->email }}</p>
|
||||
<p class="text-xs text-slate-400">{{ $member->email }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
@if($member->status === 'invited')
|
||||
<span class="rounded-full bg-amber-50 px-2.5 py-1 text-[11px] font-medium text-amber-700">Invited</span>
|
||||
@endif
|
||||
@if($canManage)
|
||||
<form method="POST" action="{{ route('account.team.role', $member) }}">
|
||||
@csrf @method('PATCH')
|
||||
<select name="role" onchange="this.form.submit()" class="rounded-lg border border-slate-200 bg-white px-2 py-1 text-xs focus:outline-none">
|
||||
<option value="member" @selected($member->role === 'member')>Member</option>
|
||||
<option value="admin" @selected($member->role === 'admin')>Admin</option>
|
||||
</select>
|
||||
</form>
|
||||
<x-confirm-dialog
|
||||
:name="'remove-member-'.$member->id"
|
||||
title="Remove team member?"
|
||||
:message="'Remove '.$member->email.' from this team?'"
|
||||
:action="route('account.team.destroy', $member)"
|
||||
method="DELETE"
|
||||
confirm-label="Remove"
|
||||
>
|
||||
<x-slot:trigger>
|
||||
<button type="button" class="text-xs font-medium text-rose-600 hover:underline">Remove</button>
|
||||
</x-slot:trigger>
|
||||
</x-confirm-dialog>
|
||||
@else
|
||||
<span class="rounded-full bg-slate-100 px-2.5 py-1 text-[11px] font-medium capitalize text-slate-600">{{ $member->role }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</li>
|
||||
@empty
|
||||
<li class="px-5 py-8 text-center text-sm text-slate-400">No teammates yet.</li>
|
||||
@endforelse
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
Reference in New Issue
Block a user