update api and add more features
This commit is contained in:
@@ -37,14 +37,19 @@ class AuthorizedUserInviteController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
$redirectUrl = (string) $result['redirect_url'];
|
||||
if (! $this->isTrustedRedirectUrl($redirectUrl)) {
|
||||
return response()->json(['message' => 'Invalid redirect URL.'], 400);
|
||||
}
|
||||
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json([
|
||||
'message' => 'Confirmed.',
|
||||
'redirect_url' => $result['redirect_url'],
|
||||
'redirect_url' => $redirectUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->away($result['redirect_url']);
|
||||
return redirect()->away($redirectUrl);
|
||||
}
|
||||
|
||||
public function setPasswordForm(Request $request, int $authorizedUserId): JsonResponse|Response
|
||||
@@ -79,7 +84,8 @@ class AuthorizedUserInviteController extends Controller
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'password' => ['required', 'string', 'min:8', 'regex:/[A-Z]/', 'regex:/[a-z]/', 'regex:/[0-9]/', 'regex:/[\W_]/'],
|
||||
'password_confirm' => ['required', 'same:password'],
|
||||
'password_confirmation' => ['required_without:password_confirm', 'same:password'],
|
||||
'password_confirm' => ['required_without:password_confirmation', 'same:password'],
|
||||
'user_id' => ['required', 'integer'],
|
||||
'token' => ['required', 'string'],
|
||||
], [
|
||||
@@ -108,4 +114,23 @@ class AuthorizedUserInviteController extends Controller
|
||||
|
||||
return response()->json(['message' => 'Password has been successfully set.']);
|
||||
}
|
||||
|
||||
private function isTrustedRedirectUrl(string $url): bool
|
||||
{
|
||||
$host = parse_url($url, PHP_URL_HOST);
|
||||
if (! is_string($host) || $host === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$trusted = [
|
||||
parse_url((string) config('app.url'), PHP_URL_HOST),
|
||||
parse_url((string) config('app.frontend_url'), PHP_URL_HOST),
|
||||
parse_url((string) config('services.frontend.url'), PHP_URL_HOST),
|
||||
];
|
||||
|
||||
return in_array(strtolower($host), array_filter(array_map(
|
||||
static fn ($value) => is_string($value) ? strtolower($value) : null,
|
||||
$trusted
|
||||
)), true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,14 +58,17 @@ class AuthorizedUsersController extends BaseApiController
|
||||
return $guard;
|
||||
}
|
||||
|
||||
$email = strtolower($request->validated('email'));
|
||||
$email = strtolower(trim((string) $request->validated('email')));
|
||||
$result = $this->svc->inviteByEmail($guard, $email);
|
||||
|
||||
$message = 'Authorized user added. A confirmation email has been sent.';
|
||||
if (! $result['created']) {
|
||||
return $this->success(['message' => $message], $message, Response::HTTP_CREATED);
|
||||
$message = 'If the email belongs to a registered user, a confirmation email has been sent.';
|
||||
|
||||
return $this->success(['message' => $message], $message, Response::HTTP_OK);
|
||||
}
|
||||
|
||||
$message = 'Authorized user added. A confirmation email has been sent.';
|
||||
|
||||
return $this->success(
|
||||
['message' => $message, 'id' => $result['record']->id],
|
||||
$message,
|
||||
@@ -89,11 +92,18 @@ class AuthorizedUsersController extends BaseApiController
|
||||
$row = $resolution;
|
||||
|
||||
$email = $request->validated('email') ?? null;
|
||||
if (is_string($email) && $email !== '') {
|
||||
$row->email = strtolower($email);
|
||||
}
|
||||
if (is_string($email) && trim($email) !== '') {
|
||||
try {
|
||||
$this->svc->changeEmailAndReinvite($row, $email);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return $this->error($e->getMessage(), Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
$row->save();
|
||||
return $this->success(
|
||||
['message' => 'Authorized user email updated. A new confirmation email has been sent.'],
|
||||
'Authorized user email updated. A new confirmation email has been sent.'
|
||||
);
|
||||
}
|
||||
|
||||
return $this->success(['message' => 'Authorized user information updated.'], 'Authorized user information updated.');
|
||||
}
|
||||
@@ -127,15 +137,15 @@ class AuthorizedUsersController extends BaseApiController
|
||||
|
||||
private function authorizedUserForParent(int $id, int $parentId): AuthorizedUser|JsonResponse
|
||||
{
|
||||
$row = AuthorizedUser::query()->find($id);
|
||||
$row = AuthorizedUser::query()
|
||||
->where('id', $id)
|
||||
->where('user_id', $parentId)
|
||||
->first();
|
||||
|
||||
if (! $row) {
|
||||
return $this->error('Authorized user not found.', Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ((int) $row->user_id !== $parentId) {
|
||||
return $this->error('You do not have access to this resource.', Response::HTTP_FORBIDDEN);
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,10 +148,14 @@ class ParentController extends BaseApiController
|
||||
$payload['emergency_contacts'] ?? []
|
||||
);
|
||||
|
||||
$createdCount = count($result['created_student_ids']);
|
||||
$skipped = $result['skipped'] ?? [];
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'created_student_ids' => $result['created_student_ids'],
|
||||
], 201);
|
||||
'skipped' => $skipped,
|
||||
], $createdCount > 0 ? 201 : 200);
|
||||
}
|
||||
|
||||
public function updateStudent(ParentStudentUpdateRequest $request, int $studentId): JsonResponse
|
||||
@@ -283,10 +287,21 @@ class ParentController extends BaseApiController
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|JsonResponse Authenticated parent user id, or 401 JSON for this controller's legacy `{ ok }` shape.
|
||||
* @return int|JsonResponse Effective primary-parent user id, or 401 JSON for this controller's legacy `{ ok }` shape.
|
||||
*
|
||||
* `parent.access` middleware resolves second parents (user_type=secondary)
|
||||
* and authorized users (user_type=tertiary) to their primary parent and
|
||||
* stashes the result as `primary_parent_id` on the request. If we are
|
||||
* called outside that middleware we fall back to the authenticated id.
|
||||
*/
|
||||
private function parentIdOrUnauthorized(): int|JsonResponse
|
||||
{
|
||||
$request = request();
|
||||
$primary = (int) ($request?->attributes?->get('primary_parent_id') ?? 0);
|
||||
if ($primary > 0) {
|
||||
return $primary;
|
||||
}
|
||||
|
||||
$parentId = (int) (auth()->id() ?? 0);
|
||||
if ($parentId <= 0) {
|
||||
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Parents;
|
||||
|
||||
use App\Http\Controllers\Api\Core\BaseApiController;
|
||||
use App\Http\Requests\Promotions\ParentEnrollmentStepRequest;
|
||||
use App\Http\Requests\Promotions\ParentPromotionOverviewRequest;
|
||||
use App\Http\Resources\Promotions\StudentPromotionResource;
|
||||
use App\Models\Student;
|
||||
use App\Models\StudentPromotionRecord;
|
||||
use App\Services\Promotions\PromotionEnrollmentService;
|
||||
use App\Services\Promotions\PromotionQueryService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Parent portal endpoints for the promotion + enrollment workflow
|
||||
* (plan section 12). The parent sees the promoted level and can step
|
||||
* through the enrollment checklist defined in plan section 8.
|
||||
*/
|
||||
class ParentPromotionController extends BaseApiController
|
||||
{
|
||||
public function __construct(
|
||||
private PromotionEnrollmentService $service,
|
||||
private PromotionQueryService $query
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function overview(ParentPromotionOverviewRequest $request): JsonResponse
|
||||
{
|
||||
$guard = $this->parentIdOrUnauthorized();
|
||||
if ($guard instanceof JsonResponse) {
|
||||
return $guard;
|
||||
}
|
||||
|
||||
$data = $this->service->overviewForParent(
|
||||
$guard,
|
||||
$request->validated()['next_school_year'] ?? null
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'records' => $data['records'],
|
||||
'next_school_year' => $data['next_school_year'],
|
||||
'message' => $data['message'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(int $promotionId): JsonResponse
|
||||
{
|
||||
$guard = $this->parentIdOrUnauthorized();
|
||||
if ($guard instanceof JsonResponse) {
|
||||
return $guard;
|
||||
}
|
||||
|
||||
$record = $this->loadOwnedRecord($promotionId, $guard);
|
||||
if ($record instanceof JsonResponse) {
|
||||
return $record;
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => new StudentPromotionResource($this->query->detail($record)),
|
||||
]);
|
||||
}
|
||||
|
||||
public function start(int $promotionId): JsonResponse
|
||||
{
|
||||
$guard = $this->parentIdOrUnauthorized();
|
||||
if ($guard instanceof JsonResponse) {
|
||||
return $guard;
|
||||
}
|
||||
|
||||
$record = $this->loadOwnedRecord($promotionId, $guard);
|
||||
if ($record instanceof JsonResponse) {
|
||||
return $record;
|
||||
}
|
||||
|
||||
try {
|
||||
$updated = $this->service->startEnrollment($record, $guard, $guard);
|
||||
} catch (\RuntimeException $e) {
|
||||
return response()->json(['ok' => false, 'message' => $e->getMessage()], Response::HTTP_CONFLICT);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => new StudentPromotionResource($this->query->detail($updated->refresh())),
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateSteps(ParentEnrollmentStepRequest $request, int $promotionId): JsonResponse
|
||||
{
|
||||
$guard = $this->parentIdOrUnauthorized();
|
||||
if ($guard instanceof JsonResponse) {
|
||||
return $guard;
|
||||
}
|
||||
|
||||
$record = $this->loadOwnedRecord($promotionId, $guard);
|
||||
if ($record instanceof JsonResponse) {
|
||||
return $record;
|
||||
}
|
||||
|
||||
try {
|
||||
$updated = $this->service->updateSteps($record, $guard, $request->steps(), $guard);
|
||||
} catch (\RuntimeException $e) {
|
||||
return response()->json(['ok' => false, 'message' => $e->getMessage()], Response::HTTP_CONFLICT);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => new StudentPromotionResource($this->query->detail($updated->refresh())),
|
||||
]);
|
||||
}
|
||||
|
||||
public function submit(int $promotionId): JsonResponse
|
||||
{
|
||||
$guard = $this->parentIdOrUnauthorized();
|
||||
if ($guard instanceof JsonResponse) {
|
||||
return $guard;
|
||||
}
|
||||
|
||||
$record = $this->loadOwnedRecord($promotionId, $guard);
|
||||
if ($record instanceof JsonResponse) {
|
||||
return $record;
|
||||
}
|
||||
|
||||
try {
|
||||
$updated = $this->service->submitEnrollment($record, $guard, $guard);
|
||||
} catch (\RuntimeException $e) {
|
||||
return response()->json(['ok' => false, 'message' => $e->getMessage()], Response::HTTP_CONFLICT);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => new StudentPromotionResource($this->query->detail($updated->refresh())),
|
||||
]);
|
||||
}
|
||||
|
||||
private function loadOwnedRecord(int $promotionId, int $parentId): StudentPromotionRecord|JsonResponse
|
||||
{
|
||||
$record = StudentPromotionRecord::query()->find($promotionId);
|
||||
if (!$record) {
|
||||
return response()->json(['ok' => false, 'message' => 'Promotion record not found.'], Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$owns = (int) $record->parent_id === $parentId;
|
||||
if (!$owns) {
|
||||
$studentParentId = (int) (Student::query()
|
||||
->where('id', $record->student_id)
|
||||
->value('parent_id') ?? 0);
|
||||
$owns = $studentParentId === $parentId;
|
||||
}
|
||||
if (!$owns) {
|
||||
return response()->json(['ok' => false, 'message' => 'You are not authorised to view this promotion record.'], Response::HTTP_FORBIDDEN);
|
||||
}
|
||||
return $record;
|
||||
}
|
||||
|
||||
private function parentIdOrUnauthorized(): int|JsonResponse
|
||||
{
|
||||
$request = request();
|
||||
$primary = (int) ($request?->attributes?->get('primary_parent_id') ?? 0);
|
||||
if ($primary > 0) {
|
||||
return $primary;
|
||||
}
|
||||
$parentId = (int) (auth()->id() ?? 0);
|
||||
if ($parentId <= 0) {
|
||||
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], Response::HTTP_UNAUTHORIZED);
|
||||
}
|
||||
return $parentId;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user