Add team, developers, PDF type, and QR rendering dependency.
Deploy Ladill QR Plus / deploy (push) Successful in 42s
Deploy Ladill QR Plus / deploy (push) Successful in 42s
Enables account collaboration, API tokens, hosted PDF codes, and fixes QR detail 500s by declaring chillerlan/php-qrcode. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\QrTeamMember;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -81,6 +82,8 @@ class SsoLoginController extends Controller
|
||||
],
|
||||
);
|
||||
|
||||
QrTeamMember::linkPendingInvitesFor($user);
|
||||
|
||||
Auth::login($user, remember: true);
|
||||
$request->session()->regenerate();
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class AccountController extends Controller
|
||||
|
||||
public function wallet(): View
|
||||
{
|
||||
$user = auth()->user();
|
||||
$user = ladill_account();
|
||||
[$balanceMinor, $ledger] = $this->billingSnapshot($user?->public_id);
|
||||
|
||||
return view('qr.account.wallet', [
|
||||
@@ -43,7 +43,7 @@ class AccountController extends Controller
|
||||
|
||||
public function billing(): View
|
||||
{
|
||||
$user = auth()->user();
|
||||
$user = ladill_account();
|
||||
[$balanceMinor, $ledger] = $this->billingSnapshot($user?->public_id);
|
||||
|
||||
return view('qr.account.billing', [
|
||||
@@ -56,6 +56,6 @@ class AccountController extends Controller
|
||||
|
||||
public function settings(): View
|
||||
{
|
||||
return view('qr.account.settings', ['account' => auth()->user()]);
|
||||
return view('qr.account.settings', ['account' => ladill_account()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Qr;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* Developers — personal API tokens for the Ladill QR Plus API (Sanctum). Tokens
|
||||
* belong to the signed-in user and authorize calls to /api/v1/*. The plaintext
|
||||
* token is shown once on create.
|
||||
*/
|
||||
class DeveloperController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
return view('qr.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'], ['qr: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.');
|
||||
}
|
||||
}
|
||||
@@ -21,20 +21,20 @@ class OverviewController extends Controller
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$user = $request->user();
|
||||
$wallet = $this->manager->walletFor($user);
|
||||
$codes = $user->qrCodes()
|
||||
$account = ladill_account();
|
||||
$wallet = $this->manager->walletFor($account);
|
||||
$codes = $account->qrCodes()
|
||||
->whereIn('type', QrTypeCatalog::plusTypes())
|
||||
->latest()
|
||||
->limit(5)
|
||||
->get();
|
||||
|
||||
$activeCount = $user->qrCodes()
|
||||
$activeCount = $account->qrCodes()
|
||||
->whereIn('type', QrTypeCatalog::plusTypes())
|
||||
->where('is_active', true)
|
||||
->count();
|
||||
|
||||
$scans30d = $user->qrCodes()
|
||||
$scans30d = $account->qrCodes()
|
||||
->whereIn('type', QrTypeCatalog::plusTypes())
|
||||
->withSum(['scanEvents as scans_30d' => fn ($q) => $q->where('created_at', '>=', now()->subDays(30))], 'id')
|
||||
->get()
|
||||
@@ -42,10 +42,10 @@ class OverviewController extends Controller
|
||||
|
||||
$balanceMinor = 0;
|
||||
try {
|
||||
$balanceMinor = $this->billing->balanceMinor($user->public_id);
|
||||
$balanceMinor = $this->billing->balanceMinor($account->public_id);
|
||||
} catch (Throwable $e) {
|
||||
Log::warning('QR Plus dashboard could not load wallet balance', [
|
||||
'user' => $user->public_id,
|
||||
'user' => $account->public_id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -40,19 +40,19 @@ class QrCodeController extends Controller
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$user = $request->user();
|
||||
$wallet = $this->manager->walletFor($user);
|
||||
$qrCodes = $user->qrCodes()
|
||||
$account = ladill_account();
|
||||
$wallet = $this->manager->walletFor($account);
|
||||
$qrCodes = $account->qrCodes()
|
||||
->whereIn('type', QrTypeCatalog::plusTypes())
|
||||
->latest()
|
||||
->get();
|
||||
|
||||
$ladillWalletBalance = 0.0;
|
||||
try {
|
||||
$ladillWalletBalance = $this->platformBilling->balanceMinor($user->public_id) / 100;
|
||||
$ladillWalletBalance = $this->platformBilling->balanceMinor($account->public_id) / 100;
|
||||
} catch (Throwable $e) {
|
||||
Log::warning('QR Plus index could not load Ladill wallet balance', [
|
||||
'user' => $user->public_id,
|
||||
'user' => $account->public_id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
@@ -69,7 +69,8 @@ class QrCodeController extends Controller
|
||||
|
||||
public function create(Request $request): View
|
||||
{
|
||||
$wallet = $this->manager->walletFor($request->user());
|
||||
$account = ladill_account();
|
||||
$wallet = $this->manager->walletFor($account);
|
||||
|
||||
return view('qr-codes.create', [
|
||||
'wallet' => $wallet,
|
||||
@@ -80,7 +81,7 @@ class QrCodeController extends Controller
|
||||
'frameStyles' => QrFrameStyleCatalog::visible(),
|
||||
'pricePerQr' => QrWallet::pricePerQr(),
|
||||
'minTopup' => QrWallet::minTopupGhs(),
|
||||
'ladillWalletBalance' => $this->platformBilling->balanceMinor($request->user()->public_id) / 100,
|
||||
'ladillWalletBalance' => $this->platformBilling->balanceMinor($account->public_id) / 100,
|
||||
'topupUrl' => 'https://'.config('app.account_domain').'/wallet',
|
||||
]);
|
||||
}
|
||||
@@ -134,7 +135,7 @@ class QrCodeController extends Controller
|
||||
}
|
||||
|
||||
try {
|
||||
$qrCode = $this->manager->create($request->user(), array_merge(
|
||||
$qrCode = $this->manager->create(ladill_account(), array_merge(
|
||||
$request->all(),
|
||||
[
|
||||
'style' => $validated['style'] ?? [],
|
||||
@@ -175,13 +176,24 @@ class QrCodeController extends Controller
|
||||
$logoDataUri = ! empty($qrStyle['logo_path'])
|
||||
? $this->imageGenerator->logoDataUri($qrStyle['logo_path'])
|
||||
: null;
|
||||
$wallet = $this->manager->walletFor($request->user());
|
||||
$account = ladill_account();
|
||||
$wallet = $this->manager->walletFor($account);
|
||||
$summary = $this->analytics->summaryFor($qrCode);
|
||||
$dailyScans = $this->analytics->dailyScans($qrCode, 30);
|
||||
$devices = $this->analytics->breakdown($qrCode, 'device_type');
|
||||
$browsers = $this->analytics->breakdown($qrCode, 'browser');
|
||||
$recentScans = $this->analytics->recentScans($qrCode);
|
||||
|
||||
$ladillWalletBalance = 0.0;
|
||||
try {
|
||||
$ladillWalletBalance = $this->platformBilling->balanceMinor($account->public_id) / 100;
|
||||
} catch (Throwable $e) {
|
||||
Log::warning('QR Plus show could not load Ladill wallet balance', [
|
||||
'user' => $account->public_id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
return view('qr-codes.show', [
|
||||
'qrCode' => $qrCode,
|
||||
'previewDataUri' => $previewDataUri,
|
||||
@@ -199,7 +211,7 @@ class QrCodeController extends Controller
|
||||
'recentScans' => $recentScans,
|
||||
'pricePerQr' => QrWallet::pricePerQr(),
|
||||
'minTopup' => QrWallet::minTopupGhs(),
|
||||
'ladillWalletBalance' => $this->platformBilling->balanceMinor($request->user()->public_id) / 100,
|
||||
'ladillWalletBalance' => $ladillWalletBalance,
|
||||
'topupUrl' => 'https://'.config('app.account_domain').'/wallet',
|
||||
]);
|
||||
}
|
||||
@@ -281,7 +293,7 @@ class QrCodeController extends Controller
|
||||
$style['logo_path'] = $tempLogoPath;
|
||||
} elseif ($request->boolean('use_existing_logo') && ! empty($validated['existing_logo_path'])) {
|
||||
$existingPath = $validated['existing_logo_path'];
|
||||
$userPrefix = $request->user()->id . '/';
|
||||
$userPrefix = ladill_account()->id . '/';
|
||||
if (str_starts_with($existingPath, $userPrefix) && Storage::disk('qr')->exists($existingPath)) {
|
||||
$style['logo_path'] = $existingPath;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Qr;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\QrTeamMember;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class TeamController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$account = ladill_account();
|
||||
|
||||
$members = QrTeamMember::query()
|
||||
->where('account_id', $account->id)
|
||||
->with('member')
|
||||
->orderBy('status')->orderBy('email')
|
||||
->get();
|
||||
|
||||
return view('qr.account.team', [
|
||||
'account' => $account,
|
||||
'members' => $members,
|
||||
'canManage' => $this->canManage($request),
|
||||
'isOwner' => $request->user()->id === $account->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
abort_unless($this->canManage($request), 403);
|
||||
|
||||
$validated = $request->validate([
|
||||
'email' => ['required', 'email', 'max:255'],
|
||||
'role' => ['required', 'in:admin,member'],
|
||||
]);
|
||||
|
||||
$account = ladill_account();
|
||||
$email = strtolower($validated['email']);
|
||||
|
||||
if ($email === strtolower($account->email)) {
|
||||
return back()->withErrors(['email' => 'The owner is already on the account.']);
|
||||
}
|
||||
|
||||
$member = QrTeamMember::firstOrNew(['account_id' => $account->id, 'email' => $email]);
|
||||
$member->role = $validated['role'];
|
||||
if (! $member->exists) {
|
||||
$member->status = QrTeamMember::STATUS_INVITED;
|
||||
$member->token = Str::random(40);
|
||||
}
|
||||
$member->save();
|
||||
|
||||
if ($existing = User::whereRaw('LOWER(email) = ?', [$email])->first()) {
|
||||
QrTeamMember::linkPendingInvitesFor($existing);
|
||||
}
|
||||
|
||||
return back()->with('success', $email.' invited.');
|
||||
}
|
||||
|
||||
public function updateRole(Request $request, QrTeamMember $member): RedirectResponse
|
||||
{
|
||||
abort_unless($this->canManage($request) && $member->account_id === ladill_account()->id, 403);
|
||||
|
||||
$validated = $request->validate(['role' => ['required', 'in:admin,member']]);
|
||||
$member->update(['role' => $validated['role']]);
|
||||
|
||||
return back()->with('success', 'Role updated.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request, QrTeamMember $member): RedirectResponse
|
||||
{
|
||||
abort_unless($this->canManage($request) && $member->account_id === ladill_account()->id, 403);
|
||||
|
||||
$member->delete();
|
||||
|
||||
return back()->with('success', 'Member removed.');
|
||||
}
|
||||
|
||||
public function switchAccount(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate(['account' => ['required', 'integer']]);
|
||||
|
||||
abort_unless($request->user()->canAccessAccount((int) $validated['account']), 403);
|
||||
|
||||
$request->session()->put('ladill_account', (int) $validated['account']);
|
||||
|
||||
return redirect()->route('qr.dashboard');
|
||||
}
|
||||
|
||||
private function canManage(Request $request): bool
|
||||
{
|
||||
$user = $request->user();
|
||||
$account = ladill_account();
|
||||
|
||||
if ($user->id === $account->id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $user->memberships()
|
||||
->where('account_id', $account->id)
|
||||
->where('role', QrTeamMember::ROLE_ADMIN)
|
||||
->exists();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\User;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class SetActingAccount
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if ($user = $request->user()) {
|
||||
$accountId = (int) $request->session()->get('ladill_account', $user->id);
|
||||
|
||||
if (! $user->canAccessAccount($accountId)) {
|
||||
$accountId = $user->id;
|
||||
$request->session()->put('ladill_account', $accountId);
|
||||
}
|
||||
|
||||
$account = $accountId === $user->id ? $user : (User::find($accountId) ?? $user);
|
||||
|
||||
$request->attributes->set('actingAccount', $account);
|
||||
View::share('actingAccount', $account);
|
||||
View::share('accessibleAccounts', $user->accessibleAccounts());
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user