318 lines
10 KiB
PHP
318 lines
10 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Http\Requests\AccountUpdateRequest;
|
|
use App\Models\Location;
|
|
use App\Models\UserAddress;
|
|
use App\Services\Location\Geocoder;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\Redirect;
|
|
use Illuminate\Support\Facades\Validator;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
|
|
class AccountController extends Controller
|
|
{
|
|
/**
|
|
* landing page
|
|
*/
|
|
public function index(): RedirectResponse
|
|
{
|
|
return Redirect::route('account.info');
|
|
}
|
|
|
|
/**
|
|
* info pane (name, email, timezone, etc.)
|
|
*/
|
|
public function infoForm(Request $request)
|
|
{
|
|
$user = $request->user();
|
|
|
|
return $this->frame('account.settings.info', [
|
|
'title' => __('Account info'),
|
|
'sub' => __('Update your personal information and sign-in email.'),
|
|
'user' => $user,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* save info pane
|
|
*/
|
|
public function infoStore(AccountUpdateRequest $request): RedirectResponse
|
|
{
|
|
$user = $request->user();
|
|
$user->fill($request->validated());
|
|
|
|
if ($user->isDirty('email')) {
|
|
$user->email_verified_at = null;
|
|
}
|
|
|
|
$user->save();
|
|
|
|
return Redirect::route('account.info')
|
|
->with('toast', __('Account updated!'));
|
|
}
|
|
|
|
/**
|
|
* addresses pane (home + work)
|
|
*/
|
|
public function addressesForm(Request $request)
|
|
{
|
|
$user = $request->user();
|
|
|
|
// try primary of each kind; fall back to the first of that kind
|
|
$home = $user->addresses()->where('kind', 'home')->where('is_primary', 1)->first()
|
|
?? $user->addresses()->where('kind', 'home')->first();
|
|
|
|
$work = $user->addresses()->where('kind', 'work')->where('is_primary', 1)->first()
|
|
?? $user->addresses()->where('kind', 'work')->first();
|
|
|
|
// if neither is explicitly billing, fall back to "work if present, else home"
|
|
$billing =
|
|
($home && $home->is_billing) ? 'home' :
|
|
(($work && $work->is_billing) ? 'work' :
|
|
($work ? 'work' : 'home'));
|
|
|
|
return $this->frame('account.settings.addresses', [
|
|
'title' => __('account.settings.addresses.title'),
|
|
'sub' => __('account.settings.addresses.subtitle'),
|
|
'user' => $user,
|
|
'home' => $home,
|
|
'work' => $work,
|
|
'billing' => $billing,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* save addresses pane
|
|
*/
|
|
public function addressesStore(Request $request, Geocoder $geocoder): RedirectResponse
|
|
{
|
|
$user = $request->user();
|
|
|
|
$data = $request->validate([
|
|
'billing' => ['required', 'in:home,work'],
|
|
|
|
'home' => ['sometimes', 'array'],
|
|
'home.label' => ['nullable', 'string', 'max:100'],
|
|
'home.line1' => ['nullable', 'string', 'max:255'],
|
|
'home.line2' => ['nullable', 'string', 'max:255'],
|
|
'home.city' => ['nullable', 'string', 'max:120'],
|
|
'home.state' => ['nullable', 'string', 'max:64'],
|
|
'home.postal' => ['nullable', 'string', 'max:32'],
|
|
'home.country' => ['nullable', 'string', 'max:64'],
|
|
|
|
'work' => ['sometimes', 'array'],
|
|
'work.label' => ['nullable', 'string', 'max:100'],
|
|
'work.line1' => ['nullable', 'string', 'max:255'],
|
|
'work.line2' => ['nullable', 'string', 'max:255'],
|
|
'work.city' => ['nullable', 'string', 'max:120'],
|
|
'work.state' => ['nullable', 'string', 'max:64'],
|
|
'work.postal' => ['nullable', 'string', 'max:32'],
|
|
'work.country' => ['nullable', 'string', 'max:64'],
|
|
]);
|
|
|
|
DB::transaction(function () use ($user, $data, $geocoder) {
|
|
|
|
$saved = []; // kind => UserAddress
|
|
|
|
$save = function (string $kind, array $payload) use ($user, $geocoder, &$saved) {
|
|
// short-circuit if nothing filled
|
|
$filled = collect($payload)
|
|
->only(['line1', 'line2', 'city', 'state', 'postal', 'country'])
|
|
->filter(fn ($v) => filled($v));
|
|
|
|
if ($filled->isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
// upsert a single primary address of this kind
|
|
$addr = UserAddress::updateOrCreate(
|
|
['user_id' => $user->id, 'kind' => $kind, 'is_primary' => 1],
|
|
[
|
|
'label' => $payload['label'] ?? ucfirst($kind),
|
|
'line1' => $payload['line1'] ?? null,
|
|
'line2' => $payload['line2'] ?? null,
|
|
'city' => $payload['city'] ?? null,
|
|
'state' => $payload['state'] ?? null,
|
|
'postal' => $payload['postal'] ?? null,
|
|
'country' => $payload['country'] ?? null,
|
|
]
|
|
);
|
|
|
|
$saved[$kind] = $addr;
|
|
|
|
// geocode
|
|
$singleLine = collect([
|
|
$addr->line1,
|
|
$addr->line2,
|
|
$addr->city,
|
|
$addr->state,
|
|
$addr->postal,
|
|
$addr->country,
|
|
])->filter()->implode(', ');
|
|
|
|
if ($singleLine === '') {
|
|
return;
|
|
}
|
|
|
|
$norm = $geocoder->forward($singleLine);
|
|
|
|
if (!$norm) {
|
|
return;
|
|
}
|
|
|
|
$loc = Location::findOrCreateNormalized($norm, $singleLine);
|
|
$addr->location_id = $loc->id;
|
|
$addr->save();
|
|
};
|
|
|
|
if (isset($data['home'])) {
|
|
$save('home', $data['home']);
|
|
}
|
|
|
|
if (isset($data['work'])) {
|
|
$save('work', $data['work']);
|
|
}
|
|
|
|
// billing flag: clear all billing flags for this user
|
|
UserAddress::where('user_id', $user->id)->update(['is_billing' => 0]);
|
|
|
|
// set billing on the chosen kind if we have that address
|
|
$kind = $data['billing'];
|
|
|
|
// if the chosen kind wasn't saved in this submission (e.g., user didn't fill it),
|
|
// try to find an existing primary address of that kind.
|
|
$billingAddr = $saved[$kind] ?? UserAddress::where('user_id', $user->id)
|
|
->where('kind', $kind)
|
|
->where('is_primary', 1)
|
|
->first();
|
|
|
|
if ($billingAddr) {
|
|
$billingAddr->is_billing = 1;
|
|
$billingAddr->save();
|
|
}
|
|
});
|
|
|
|
return Redirect::route('account.addresses')
|
|
->with('toast', __('Addresses updated!'));
|
|
}
|
|
|
|
/**
|
|
* password pane
|
|
*/
|
|
public function passwordForm(Request $request)
|
|
{
|
|
return $this->frame('account.settings.password', [
|
|
'title' => __('Password'),
|
|
'sub' => __('Change your password.'),
|
|
'user' => $request->user(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* save password pane
|
|
*/
|
|
public function passwordStore(Request $request): RedirectResponse
|
|
{
|
|
$data = $request->validate([
|
|
'current_password' => ['required', 'current_password'],
|
|
'password' => ['required', 'string', 'min:12', 'confirmed'],
|
|
]);
|
|
|
|
$user = $request->user();
|
|
|
|
$user->forceFill([
|
|
'password' => Hash::make($data['password']),
|
|
])->save();
|
|
|
|
return Redirect::route('account.password')
|
|
->with('toast', __('Password updated!'));
|
|
}
|
|
|
|
/**
|
|
* delete pane
|
|
*/
|
|
public function deleteForm(Request $request)
|
|
{
|
|
return $this->frame('account.settings.delete', [
|
|
'title' => __('account.settings.delete.title'),
|
|
'sub' => __('account.settings.delete.subtitle'),
|
|
'user' => $request->user(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* delete confirmation modal/page
|
|
*/
|
|
public function deleteConfirm(Request $request)
|
|
{
|
|
if ($request->header('HX-Request')) {
|
|
return view('account.partials.delete-modal');
|
|
}
|
|
|
|
// no-js full page
|
|
return $this->frame('account.settings.delete-confirm', [
|
|
'title' => __('account.settings.delete-confirm.title'),
|
|
'sub' => __('account.settings.delete-confirm.subtitle'),
|
|
'user' => $request->user(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* delete the user's account
|
|
*/
|
|
public function destroy(Request $request)
|
|
{
|
|
// validate: on failure
|
|
// - full page: redirects to confirm page, flashes errors + input + toast
|
|
// - htmx: returns 422 modal html + HX-Trigger toast
|
|
$this->validateHtmx($request, [
|
|
'password' => ['required', 'current_password'],
|
|
], [
|
|
'bag' => 'userDeletion',
|
|
'redirect' => 'account.delete.confirm', // no-js failure lands back on confirm
|
|
'htmx_view' => 'account.modals.delete', // modal content to re-render on 422
|
|
'toast_type' => 'error', // optional, default is already error
|
|
// optional: override toast message (default is first validation message)
|
|
// 'toast_message' => __('Please correct the password and try again.'),
|
|
// optional: disable toast on validation failures:
|
|
// 'toast' => false,
|
|
]);
|
|
|
|
// passed validation, delete account
|
|
$user = $request->user();
|
|
|
|
Auth::logout();
|
|
$user->delete();
|
|
|
|
$request->session()->invalidate();
|
|
$request->session()->regenerateToken();
|
|
|
|
// success: for no-js, redirect home with toast
|
|
// for htmx, send HX-Redirect + HX-Trigger (handled inside helper)
|
|
return $this->redirectWithToast(
|
|
$request,
|
|
'dashboard', // or whatever route you want, could be a url('/') helper variant too
|
|
__('Your account has been deleted.'),
|
|
'success'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* content frame handler
|
|
*/
|
|
private function frame(?string $view = null, array $data = [])
|
|
{
|
|
return view('account.index', [
|
|
'view' => $view,
|
|
'data' => $data,
|
|
]);
|
|
}
|
|
}
|