Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions app/Http/Controllers/Account/AuthController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

namespace App\Http\Controllers\Account;

use App\Http\Controllers\Controller;
use App\Http\Requests\LoginRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;

class AuthController extends Controller
{
public function login()
{
return view('account.auth.login');
}

public function logout()
{
auth()->logout();
session()->regenerateToken();

return redirect()->route('account.login');
}

/**
* Process the login request.
*
* @TODO Implement additional brute-force protection with custom blocked IPs model.
*
* @param LoginRequest $request
* @throws \Illuminate\Validation\ValidationException
* @return \Illuminate\Http\RedirectResponse
*/
public function processLogin(LoginRequest $request)
{
$credentials = $request->only('email', 'password');
$key = 'login-attempt:' . $request->ip();
$attemptsPerHour = 5;

if (\RateLimiter::tooManyAttempts($key, $attemptsPerHour)) {
$blockedUntil = Carbon::now()
->addSeconds(\RateLimiter::availableIn($key))
->diffInMinutes(Carbon::now());

return back()
->withInput($request->only(['email', 'remember']))
->withErrors([
'email' => 'Too many login attempts. Please try again in '
. $blockedUntil . ' minutes.',
]);
}

if (auth()->attempt($credentials, $request->boolean('remember'))) {
session()->regenerate();

\RateLimiter::clear($key);

return redirect()->intended('/account');
}

\RateLimiter::increment($key, 3600);

return back()
->withInput($request->only('email'))
->withErrors([
'email' => 'The provided credentials do not match our records.',
]);
}
}
45 changes: 45 additions & 0 deletions app/Http/Controllers/Account/Support/TicketController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace App\Http\Controllers\Account\Support;

use App\Http\Controllers\Controller;
use App\Models\SupportTicket;
use App\SupportTicket\Status;
use Illuminate\Http\Request;

class TicketController extends Controller
{
public static string $paginationLimit = '10';

public function closeTicket(SupportTicket $supportTicket)
{
$this->authorize('closeTicket', $supportTicket);

$supportTicket->update([
'status' => Status::CLOSED,
]);

return redirect()
->route('support.tickets.show', $supportTicket)
->with('success', __('account.support_ticket.close_ticket.success'));
}

public function index()
{
$supportTickets = SupportTicket::whereUserId(auth()->user()->id)
->orderBy('status', 'desc')
->orderBy('created_at', 'desc')
->paginate(static::$paginationLimit);

return view('support.tickets.index', compact('supportTickets'));
}

public function show(SupportTicket $supportTicket)
{
$this->authorize('view', $supportTicket);

$supportTicket->load('user');

return view('support.tickets.show', compact('supportTicket'));
}
}
29 changes: 29 additions & 0 deletions app/Http/Requests/LoginRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class LoginRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}

/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'email' => 'required|email',
'password' => 'required|string',
];
}
}
55 changes: 55 additions & 0 deletions app/Models/SupportTicket.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace App\Models;

use App\Models\SupportTicket\Reply;
use App\SupportTicket\Status;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;

class SupportTicket extends Model
{
use HasFactory, SoftDeletes;

protected $fillable = [
'user_id',
'mask',
'subject',
'message',
'status',
];

protected $casts = [
'status' => Status::class,
];

protected static function booted()
{
static::creating(function ($ticket) {
if (is_null($ticket->mask)) {
// @TODO Generate a unique mask for the ticket
$ticket->mask = uniqid('ticket_');
}
});
}

public function getRouteKeyName(): string
{
return 'mask';
}

public function replies(): HasMany
{
return $this->hasMany(Reply::class)
->orderBy('created_at', 'desc');
}

public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
50 changes: 50 additions & 0 deletions app/Models/SupportTicket/Reply.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace App\Models\SupportTicket;

use App\Models\SupportTicket;
use App\Models\User;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;

class Reply extends Model
{
use HasFactory, SoftDeletes;

protected $fillable = [
'support_ticket_id',
'message',
'attachments',
'note',
];

protected $casts = [
'attachments' => 'array',
'note' => 'boolean',
];

public function isFromAdmin(): Attribute
{
return Attribute::get(fn () => $this->user->is_admin)
->shouldCache();
}

public function isFromUser(): Attribute
{
return Attribute::get(fn () => $this->user_id === auth()->user()->id)
->shouldCache();
}

public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}

public function supportTicket(): BelongsTo
{
return $this->belongsTo(SupportTicket::class);
}
}
7 changes: 7 additions & 0 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Cashier\Billable;
Expand All @@ -22,6 +23,12 @@ class User extends Authenticatable

protected $casts = [
'email_verified_at' => 'datetime',
'is_admin' => 'boolean',
'password' => 'hashed',
];

public function supportTickets(): HasMany
{
return $this->hasMany(SupportTicket::class);
}
}
74 changes: 74 additions & 0 deletions app/Policies/SupportTicketPolicy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

namespace App\Policies;

use App\Models\SupportTicket;
use App\Models\User;
use Illuminate\Auth\Access\Response;

class SupportTicketPolicy
{
public function closeTicket(User $user, SupportTicket $supportTicket): bool
{
return $supportTicket->user_id === $user->id;
}

/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return false;
}

/**
* Determine whether the user can view the model.
*/
public function view(User $user, SupportTicket $supportTicket): Response
{
return $user->id === $supportTicket->user_id
? Response::allow()
: Response::denyAsNotFound('Ticket not found.');
}

/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return true;
}

/**
* Determine whether the user can update the model.
*/
public function update(User $user, SupportTicket $supportTicket): bool
{
return $user->id === $supportTicket->user_id;
}

/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, SupportTicket $supportTicket): bool
{
// Deletion not allowed.
return false;
}

/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, SupportTicket $supportTicket): bool
{
return false;
}

/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, SupportTicket $supportTicket): bool
{
return false;
}
}
7 changes: 6 additions & 1 deletion app/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
namespace App\Providers;

use App\Support\GitHub;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;

Expand All @@ -24,7 +27,7 @@ public function boot(): void
$this->registerSharedViewVariables();
}

private function registerSharedViewVariables(): void
private function registerSharedViewVariables(): static
{
View::share('electronGitHubVersion', app()->environment('production')
? GitHub::electron()->latestVersion()
Expand All @@ -34,5 +37,7 @@ private function registerSharedViewVariables(): void
View::share('bskyLink', 'https://bsky.app/profile/nativephp.bsky.social');
View::share('openCollectiveLink', 'https://opencollective.com/nativephp');
View::share('githubLink', 'https://github.com/NativePHP');

return $this;
}
}
2 changes: 1 addition & 1 deletion app/Providers/RouteServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class RouteServiceProvider extends ServiceProvider
*
* @var string
*/
public const HOME = '/home';
public const HOME = '/account';

/**
* Define your route model bindings, pattern filters, and other route configuration.
Expand Down
Loading
Loading