add notifications logic and add support of both JWT and Sanctum
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Auth;
|
||||
|
||||
use App\Http\Controllers\Api\BaseApiController;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use PHPOpenSourceSaver\JWTAuth\Facades\JWTAuth;
|
||||
|
||||
class AuthController extends BaseApiController
|
||||
{
|
||||
public function login(Request $request): JsonResponse
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'email' => ['required', 'email'],
|
||||
'password' => ['required', 'string'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return $this->respondValidationError($validator->errors()->toArray());
|
||||
}
|
||||
|
||||
$email = (string) $request->input('email');
|
||||
$password = (string) $request->input('password');
|
||||
|
||||
$user = User::query()->where('email', $email)->first();
|
||||
if (!$user || !ci_password_verify($password, (string) $user->password)) {
|
||||
return $this->respondError('Invalid credentials.', 401);
|
||||
}
|
||||
|
||||
if ((int) ($user->is_verified ?? 0) !== 1) {
|
||||
return $this->respondError('Account not verified.', 403);
|
||||
}
|
||||
|
||||
if (strcasecmp((string) ($user->status ?? ''), 'Active') !== 0) {
|
||||
return $this->respondError('Account is inactive.', 403);
|
||||
}
|
||||
|
||||
$jwtToken = JWTAuth::fromUser($user);
|
||||
$sanctumToken = $user->createToken('api')->plainTextToken;
|
||||
|
||||
return $this->respondSuccess([
|
||||
'user' => [
|
||||
'id' => (int) $user->id,
|
||||
'firstname' => $user->firstname,
|
||||
'lastname' => $user->lastname,
|
||||
'email' => $user->email,
|
||||
],
|
||||
'jwt' => [
|
||||
'access_token' => $jwtToken,
|
||||
'token_type' => 'bearer',
|
||||
'expires_in' => (int) config('jwt.ttl') * 60,
|
||||
],
|
||||
'sanctum' => [
|
||||
'access_token' => $sanctumToken,
|
||||
'token_type' => 'bearer',
|
||||
],
|
||||
], 'Authenticated.');
|
||||
}
|
||||
|
||||
public function refresh(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$newToken = JWTAuth::parseToken()->refresh();
|
||||
} catch (\Throwable $e) {
|
||||
return $this->respondError('Token refresh failed.', 401);
|
||||
}
|
||||
|
||||
return $this->respondSuccess([
|
||||
'access_token' => $newToken,
|
||||
'token_type' => 'bearer',
|
||||
'expires_in' => (int) config('jwt.ttl') * 60,
|
||||
], 'Token refreshed.');
|
||||
}
|
||||
|
||||
public function me(): JsonResponse
|
||||
{
|
||||
$user = Auth::user();
|
||||
if (!$user) {
|
||||
return $this->respondError('Unauthorized.', 401);
|
||||
}
|
||||
|
||||
return $this->respondSuccess([
|
||||
'id' => (int) $user->id,
|
||||
'firstname' => $user->firstname,
|
||||
'lastname' => $user->lastname,
|
||||
'email' => $user->email,
|
||||
], 'OK');
|
||||
}
|
||||
|
||||
public function logout(Request $request): JsonResponse
|
||||
{
|
||||
$token = $request->bearerToken();
|
||||
if ($token && substr_count($token, '.') === 2) {
|
||||
try {
|
||||
JWTAuth::setToken($token)->invalidate();
|
||||
} catch (\Throwable $e) {
|
||||
// ignore invalidation errors
|
||||
}
|
||||
}
|
||||
|
||||
$user = $request->user();
|
||||
if ($user && method_exists($user, 'currentAccessToken')) {
|
||||
$current = $user->currentAccessToken();
|
||||
if ($current) {
|
||||
$current->delete();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->respondSuccess(null, 'Logged out.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Notifications;
|
||||
|
||||
use App\Http\Controllers\Api\BaseApiController;
|
||||
use App\Http\Requests\Notifications\NotificationActiveRequest;
|
||||
use App\Http\Requests\Notifications\NotificationDeletedRequest;
|
||||
use App\Http\Requests\Notifications\NotificationIndexRequest;
|
||||
use App\Http\Requests\Notifications\NotificationSendRequest;
|
||||
use App\Http\Requests\Notifications\NotificationUpdateRequest;
|
||||
use App\Http\Resources\Notifications\NotificationResource;
|
||||
use App\Http\Resources\Notifications\UserNotificationResource;
|
||||
use App\Services\Notifications\NotificationActiveService;
|
||||
use App\Services\Notifications\NotificationDeletedService;
|
||||
use App\Services\Notifications\NotificationManagementService;
|
||||
use App\Services\Notifications\NotificationReadService;
|
||||
use App\Services\Notifications\NotificationSendService;
|
||||
use App\Services\Notifications\NotificationShowService;
|
||||
use App\Services\Notifications\NotificationUserListService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class NotificationController extends BaseApiController
|
||||
{
|
||||
public function __construct(
|
||||
private NotificationUserListService $listService,
|
||||
private NotificationSendService $sendService,
|
||||
private NotificationReadService $readService,
|
||||
private NotificationActiveService $activeService,
|
||||
private NotificationDeletedService $deletedService,
|
||||
private NotificationManagementService $managementService,
|
||||
private NotificationShowService $showService
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function index(NotificationIndexRequest $request): JsonResponse
|
||||
{
|
||||
$userId = (int) (auth()->id() ?? 0);
|
||||
if ($userId <= 0) {
|
||||
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
|
||||
}
|
||||
|
||||
$payload = $request->validated();
|
||||
$result = $this->listService->listForUser($userId, $payload);
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'notifications' => UserNotificationResource::collection($result['notifications']),
|
||||
'pagination' => $result['pagination'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(int $notificationId): JsonResponse
|
||||
{
|
||||
$userId = (int) (auth()->id() ?? 0);
|
||||
if ($userId <= 0) {
|
||||
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
|
||||
}
|
||||
|
||||
$row = $this->showService->getForUser($userId, $notificationId);
|
||||
if (!$row) {
|
||||
return response()->json(['ok' => false, 'message' => 'Notification not found.'], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'notification' => new UserNotificationResource($row),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(NotificationSendRequest $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$actorId = (int) (auth()->id() ?? 0);
|
||||
|
||||
try {
|
||||
$result = $this->sendService->send($payload, $actorId);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Notification send failed.', ['error' => $e->getMessage()]);
|
||||
return response()->json(['ok' => false, 'message' => 'Failed to send notification.'], 500);
|
||||
}
|
||||
|
||||
if (empty($result['ok'])) {
|
||||
return response()->json(['ok' => false, 'message' => 'Failed to send notification.'], 422);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'notification' => new NotificationResource($result['notification']),
|
||||
'recipient_count' => $result['recipient_count'] ?? 0,
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function update(NotificationUpdateRequest $request, int $notificationId): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$result = $this->managementService->update($notificationId, $payload);
|
||||
|
||||
if (empty($result['ok'])) {
|
||||
return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Failed to update notification.'], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'notification' => new NotificationResource($result['notification']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(int $notificationId): JsonResponse
|
||||
{
|
||||
$deleted = $this->managementService->delete($notificationId);
|
||||
if (!$deleted) {
|
||||
return response()->json(['ok' => false, 'message' => 'Notification not found.'], 404);
|
||||
}
|
||||
|
||||
return response()->json(['ok' => true]);
|
||||
}
|
||||
|
||||
public function restore(int $notificationId): JsonResponse
|
||||
{
|
||||
$restored = $this->managementService->restore($notificationId);
|
||||
if (!$restored) {
|
||||
return response()->json(['ok' => false, 'message' => 'Notification not found.'], 404);
|
||||
}
|
||||
|
||||
return response()->json(['ok' => true]);
|
||||
}
|
||||
|
||||
public function markRead(int $notificationId): JsonResponse
|
||||
{
|
||||
$userId = (int) (auth()->id() ?? 0);
|
||||
if ($userId <= 0) {
|
||||
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
|
||||
}
|
||||
|
||||
$ok = $this->readService->markRead($userId, $notificationId);
|
||||
if (!$ok) {
|
||||
return response()->json(['ok' => false, 'message' => 'Notification not found.'], 404);
|
||||
}
|
||||
|
||||
return response()->json(['ok' => true]);
|
||||
}
|
||||
|
||||
public function active(NotificationActiveRequest $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$data = $this->activeService->list($payload['target_group'] ?? null);
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'notifications' => $data['notifications'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function deleted(NotificationDeletedRequest $request): JsonResponse
|
||||
{
|
||||
$data = $this->deletedService->list();
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'notifications' => $data['notifications'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use PHPOpenSourceSaver\JWTAuth\Exceptions\JWTException;
|
||||
use PHPOpenSourceSaver\JWTAuth\Facades\JWTAuth;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class MultiAuth
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$sanctumUser = Auth::guard('sanctum')->user();
|
||||
if ($sanctumUser) {
|
||||
Auth::setUser($sanctumUser);
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$token = $request->bearerToken();
|
||||
if ($token) {
|
||||
try {
|
||||
$jwtUser = JWTAuth::setToken($token)->authenticate();
|
||||
if ($jwtUser) {
|
||||
Auth::setUser($jwtUser);
|
||||
return $next($request);
|
||||
}
|
||||
} catch (JWTException $e) {
|
||||
// fall through to unauthorized response
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Unauthorized.'], 401);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Notifications;
|
||||
|
||||
use App\Http\Requests\ApiFormRequest;
|
||||
|
||||
class NotificationActiveRequest extends ApiFormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'target_group' => ['nullable', 'string', 'max:50'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Notifications;
|
||||
|
||||
use App\Http\Requests\ApiFormRequest;
|
||||
|
||||
class NotificationDeletedRequest extends ApiFormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Notifications;
|
||||
|
||||
use App\Http\Requests\ApiFormRequest;
|
||||
|
||||
class NotificationIndexRequest extends ApiFormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'read' => ['nullable', 'boolean'],
|
||||
'page' => ['nullable', 'integer', 'min:1'],
|
||||
'per_page' => ['nullable', 'integer', 'min:1', 'max:200'],
|
||||
'sort_by' => ['nullable', 'string', 'in:created_at,scheduled_at,priority'],
|
||||
'sort_dir' => ['nullable', 'string', 'in:asc,desc'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Notifications;
|
||||
|
||||
use App\Http\Requests\ApiFormRequest;
|
||||
|
||||
class NotificationSendRequest extends ApiFormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => ['required', 'string', 'max:150'],
|
||||
'message' => ['required', 'string', 'max:5000'],
|
||||
'target_group' => ['required', 'string', 'max:50'],
|
||||
'channels' => ['required', 'array'],
|
||||
'channels.*' => ['string', 'in:in_app,email,sms'],
|
||||
'priority' => ['nullable', 'string', 'max:20'],
|
||||
'status' => ['nullable', 'string', 'max:20'],
|
||||
'action_url' => ['nullable', 'string', 'max:255'],
|
||||
'attachment_path' => ['nullable', 'string', 'max:255'],
|
||||
'scheduled_at' => ['nullable', 'date'],
|
||||
'expires_at' => ['nullable', 'date', 'after_or_equal:scheduled_at'],
|
||||
'school_year' => ['nullable', 'string', 'max:20'],
|
||||
'semester' => ['nullable', 'string', 'max:20'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Notifications;
|
||||
|
||||
use App\Http\Requests\ApiFormRequest;
|
||||
|
||||
class NotificationUpdateRequest extends ApiFormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => ['sometimes', 'required', 'string', 'max:150'],
|
||||
'message' => ['sometimes', 'required', 'string', 'max:5000'],
|
||||
'target_group' => ['sometimes', 'required', 'string', 'max:50'],
|
||||
'channels' => ['sometimes', 'required', 'array'],
|
||||
'channels.*' => ['string', 'in:in_app,email,sms'],
|
||||
'priority' => ['nullable', 'string', 'max:20'],
|
||||
'status' => ['nullable', 'string', 'max:20'],
|
||||
'action_url' => ['nullable', 'string', 'max:255'],
|
||||
'attachment_path' => ['nullable', 'string', 'max:255'],
|
||||
'scheduled_at' => ['nullable', 'date'],
|
||||
'expires_at' => ['nullable', 'date', 'after_or_equal:scheduled_at'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Notifications;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class NotificationResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => (int) ($this->id ?? 0),
|
||||
'title' => $this->title,
|
||||
'message' => $this->message,
|
||||
'target_group' => $this->target_group,
|
||||
'delivery_channels' => $this->delivery_channels,
|
||||
'priority' => $this->priority,
|
||||
'status' => $this->status,
|
||||
'action_url' => $this->action_url,
|
||||
'attachment_path' => $this->attachment_path,
|
||||
'scheduled_at' => optional($this->scheduled_at)->toDateTimeString(),
|
||||
'sent_at' => optional($this->sent_at)->toDateTimeString(),
|
||||
'expires_at' => optional($this->expires_at)->toDateTimeString(),
|
||||
'school_year' => $this->school_year,
|
||||
'semester' => $this->semester,
|
||||
'created_at' => optional($this->created_at)->toDateTimeString(),
|
||||
'updated_at' => optional($this->updated_at)->toDateTimeString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Notifications;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class UserNotificationResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$scheduledAt = $this->scheduled_at ?? $this['scheduled_at'] ?? null;
|
||||
$expiresAt = $this->expires_at ?? $this['expires_at'] ?? null;
|
||||
|
||||
return [
|
||||
'notification_id' => (int) ($this->notification_id ?? $this['notification_id'] ?? 0),
|
||||
'title' => $this->title ?? $this['title'] ?? null,
|
||||
'message' => $this->message ?? $this['message'] ?? null,
|
||||
'priority' => $this->priority ?? $this['priority'] ?? null,
|
||||
'scheduled_at' => $scheduledAt ? date('Y-m-d H:i:s', strtotime((string) $scheduledAt)) : null,
|
||||
'expires_at' => $expiresAt ? date('Y-m-d H:i:s', strtotime((string) $expiresAt)) : null,
|
||||
'target_group' => $this->target_group ?? $this['target_group'] ?? null,
|
||||
'is_read' => (bool) ($this->is_read ?? $this['is_read'] ?? false),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user