diff options
Diffstat (limited to 'services/app/src/Forms')
| -rw-r--r-- | services/app/src/Forms/AdminQuizEditForm.php | 214 | ||||
| -rw-r--r-- | services/app/src/Forms/AdminQuizNewForm.php | 192 | ||||
| -rw-r--r-- | services/app/src/Forms/AdminTestcaseEditForm.php | 122 | ||||
| -rw-r--r-- | services/app/src/Forms/AdminTestcaseNewForm.php | 107 | ||||
| -rw-r--r-- | services/app/src/Forms/AdminUserEditForm.php | 85 | ||||
| -rw-r--r-- | services/app/src/Forms/AnswerNewForm.php | 116 | ||||
| -rw-r--r-- | services/app/src/Forms/LoginForm.php | 116 |
7 files changed, 952 insertions, 0 deletions
diff --git a/services/app/src/Forms/AdminQuizEditForm.php b/services/app/src/Forms/AdminQuizEditForm.php new file mode 100644 index 0000000..0fc382b --- /dev/null +++ b/services/app/src/Forms/AdminQuizEditForm.php @@ -0,0 +1,214 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\Albatross\Forms; + +use DateTimeImmutable; +use DateTimeZone; +use Nsfisis\Albatross\Exceptions\EntityValidationException; +use Nsfisis\Albatross\Form\FormBase; +use Nsfisis\Albatross\Form\FormItem; +use Nsfisis\Albatross\Form\FormState; +use Nsfisis\Albatross\Form\FormSubmissionFailureException; +use Nsfisis\Albatross\Models\Quiz; +use Nsfisis\Albatross\Repositories\QuizRepository; +use Slim\Interfaces\RouteParserInterface; + +final class AdminQuizEditForm extends FormBase +{ + public function __construct( + ?FormState $state, + private readonly Quiz $quiz, + private readonly RouteParserInterface $routeParser, + private readonly QuizRepository $quizRepo, + ) { + if (!isset($state)) { + $state = new FormState([ + 'title' => $quiz->title, + 'slug' => $quiz->slug, + 'description' => $quiz->description, + 'example_code' => $quiz->example_code, + 'birdie_code_size' => (string)$quiz->birdie_code_size, + 'started_at' => $quiz->started_at->setTimezone(new DateTimeZone('Asia/Tokyo'))->format('Y-m-d\TH:i'), + 'ranking_hidden_at' => $quiz->ranking_hidden_at->setTimezone(new DateTimeZone('Asia/Tokyo'))->format('Y-m-d\TH:i'), + 'finished_at' => $quiz->finished_at->setTimezone(new DateTimeZone('Asia/Tokyo'))->format('Y-m-d\TH:i'), + ]); + } + parent::__construct($state); + } + + public function pageTitle(): string + { + return "管理画面 - 問題 #{$this->quiz->quiz_id} - 編集"; + } + + public function redirectUrl(): string + { + return $this->routeParser->urlFor('admin_quiz_list'); + } + + protected function submitLabel(): string + { + return '保存'; + } + + /** + * @return list<FormItem> + */ + protected function items(): array + { + return [ + new FormItem( + name: 'title', + type: 'text', + label: 'タイトル', + isRequired: true, + ), + new FormItem( + name: 'slug', + type: 'text', + label: 'スラグ', + isRequired: true, + isDisabled: true, + ), + new FormItem( + name: 'description', + type: 'textarea', + label: '説明', + isRequired: true, + extra: 'rows="3" cols="80"', + ), + new FormItem( + name: 'example_code', + type: 'textarea', + label: '実装例', + isRequired: true, + extra: 'rows="10" cols="80"', + ), + new FormItem( + name: 'birdie_code_size', + type: 'text', + label: 'バーディになるコードサイズ (byte)', + ), + new FormItem( + name: 'started_at', + type: 'datetime-local', + label: '開始日時 (JST)', + isRequired: true, + ), + new FormItem( + name: 'ranking_hidden_at', + type: 'datetime-local', + label: 'ランキングが非表示になる日時 (JST)', + isRequired: true, + ), + new FormItem( + name: 'finished_at', + type: 'datetime-local', + label: '終了日時 (JST)', + isRequired: true, + ), + ]; + } + + /** + * @return array{quiz: Quiz} + */ + public function getRenderContext(): array + { + return [ + 'quiz' => $this->quiz, + ]; + } + + public function submit(): void + { + $title = $this->state->get('title') ?? ''; + $slug = $this->state->get('slug') ?? ''; + $description = $this->state->get('description') ?? ''; + $example_code = $this->state->get('example_code') ?? ''; + $birdie_code_size = $this->state->get('birdie_code_size') ?? ''; + $started_at = $this->state->get('started_at') ?? ''; + $ranking_hidden_at = $this->state->get('ranking_hidden_at') ?? ''; + $finished_at = $this->state->get('finished_at') ?? ''; + + $errors = []; + if ($birdie_code_size !== '' && !is_numeric($birdie_code_size)) { + $errors['birdie_code_size'] = '数値を入力してください'; + $birdie_code_size = ''; // dummy + } + if ($started_at === '') { + $errors['started_at'] = '開始日時は必須です'; + } else { + $started_at = DateTimeImmutable::createFromFormat( + 'Y-m-d\TH:i', + $started_at, + new DateTimeZone('Asia/Tokyo'), + ); + if ($started_at === false) { + $errors['started_at'] = '開始日時の形式が不正です'; + } else { + $started_at = $started_at->setTimezone(new DateTimeZone('UTC')); + } + } + if (!$started_at instanceof DateTimeImmutable) { + $started_at = new DateTimeImmutable('now', new DateTimeZone('UTC')); // dummy + } + if ($ranking_hidden_at === '') { + $errors['ranking_hidden_at'] = 'ランキングが非表示になる日時は必須です'; + } else { + $ranking_hidden_at = DateTimeImmutable::createFromFormat( + 'Y-m-d\TH:i', + $ranking_hidden_at, + new DateTimeZone('Asia/Tokyo'), + ); + if ($ranking_hidden_at === false) { + $errors['ranking_hidden_at'] = 'ランキングが非表示になる日時の形式が不正です'; + } else { + $ranking_hidden_at = $ranking_hidden_at->setTimezone(new DateTimeZone('UTC')); + } + } + if (!$ranking_hidden_at instanceof DateTimeImmutable) { + $ranking_hidden_at = new DateTimeImmutable('now', new DateTimeZone('UTC')); // dummy + } + if ($finished_at === '') { + $errors['finished_at'] = '終了日時は必須です'; + } else { + $finished_at = DateTimeImmutable::createFromFormat( + 'Y-m-d\TH:i', + $finished_at, + new DateTimeZone('Asia/Tokyo'), + ); + if ($finished_at === false) { + $errors['finished_at'] = '終了日時の形式が不正です'; + } else { + $finished_at = $finished_at->setTimezone(new DateTimeZone('UTC')); + } + } + if (!$finished_at instanceof DateTimeImmutable) { + $finished_at = new DateTimeImmutable('now', new DateTimeZone('UTC')); // dummy + } + + if (0 < count($errors)) { + $this->state->setErrors($errors); + throw new FormSubmissionFailureException(); + } + + try { + $this->quizRepo->update( + $this->quiz->quiz_id, + $title, + $description, + $example_code, + $birdie_code_size === '' ? null : (int)$birdie_code_size, + $started_at, + $ranking_hidden_at, + $finished_at, + ); + } catch (EntityValidationException $e) { + $this->state->setErrors($e->toFormErrors()); + throw new FormSubmissionFailureException(previous: $e); + } + } +} diff --git a/services/app/src/Forms/AdminQuizNewForm.php b/services/app/src/Forms/AdminQuizNewForm.php new file mode 100644 index 0000000..0038dc1 --- /dev/null +++ b/services/app/src/Forms/AdminQuizNewForm.php @@ -0,0 +1,192 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\Albatross\Forms; + +use DateTimeImmutable; +use DateTimeZone; +use Nsfisis\Albatross\Exceptions\EntityValidationException; +use Nsfisis\Albatross\Form\FormBase; +use Nsfisis\Albatross\Form\FormItem; +use Nsfisis\Albatross\Form\FormState; +use Nsfisis\Albatross\Form\FormSubmissionFailureException; +use Nsfisis\Albatross\Repositories\QuizRepository; +use Slim\Interfaces\RouteParserInterface; + +final class AdminQuizNewForm extends FormBase +{ + public function __construct( + ?FormState $state, + private readonly RouteParserInterface $routeParser, + private readonly QuizRepository $quizRepo, + ) { + if (!isset($state)) { + $state = new FormState(); + } + parent::__construct($state); + } + + public function pageTitle(): string + { + return '管理画面 - 問題作成'; + } + + public function redirectUrl(): string + { + return $this->routeParser->urlFor('admin_quiz_list'); + } + + protected function submitLabel(): string + { + return '作成'; + } + + /** + * @return list<FormItem> + */ + protected function items(): array + { + return [ + new FormItem( + name: 'title', + type: 'text', + label: 'タイトル', + isRequired: true, + ), + new FormItem( + name: 'slug', + type: 'text', + label: 'スラグ', + isRequired: true, + ), + new FormItem( + name: 'description', + type: 'textarea', + label: '説明', + isRequired: true, + extra: 'rows="3" cols="80"', + ), + new FormItem( + name: 'example_code', + type: 'textarea', + label: '実装例', + isRequired: true, + extra: 'rows="10" cols="80"', + ), + new FormItem( + name: 'birdie_code_size', + type: 'text', + label: 'バーディになるコードサイズ (byte)', + ), + new FormItem( + name: 'started_at', + type: 'datetime-local', + label: '開始日時 (JST)', + isRequired: true, + ), + new FormItem( + name: 'ranking_hidden_at', + type: 'datetime-local', + label: 'ランキングが非表示になる日時 (JST)', + isRequired: true, + ), + new FormItem( + name: 'finished_at', + type: 'datetime-local', + label: '終了日時 (JST)', + isRequired: true, + ), + ]; + } + + public function submit(): void + { + $title = $this->state->get('title') ?? ''; + $slug = $this->state->get('slug') ?? ''; + $description = $this->state->get('description') ?? ''; + $example_code = $this->state->get('example_code') ?? ''; + $birdie_code_size = $this->state->get('birdie_code_size') ?? ''; + $started_at = $this->state->get('started_at') ?? ''; + $ranking_hidden_at = $this->state->get('ranking_hidden_at') ?? ''; + $finished_at = $this->state->get('finished_at') ?? ''; + + $errors = []; + if ($birdie_code_size !== '' && !is_numeric($birdie_code_size)) { + $errors['birdie_code_size'] = '数値を入力してください'; + $birdie_code_size = ''; // dummy + } + if ($started_at === '') { + $errors['started_at'] = '開始日時は必須です'; + } else { + $started_at = DateTimeImmutable::createFromFormat( + 'Y-m-d\TH:i', + $started_at, + new DateTimeZone('Asia/Tokyo'), + ); + if ($started_at === false) { + $errors['started_at'] = '開始日時の形式が不正です'; + } else { + $started_at = $started_at->setTimezone(new DateTimeZone('UTC')); + } + } + if (!$started_at instanceof DateTimeImmutable) { + $started_at = new DateTimeImmutable('now', new DateTimeZone('UTC')); // dummy + } + if ($ranking_hidden_at === '') { + $errors['ranking_hidden_at'] = 'ランキングが非表示になる日時は必須です'; + } else { + $ranking_hidden_at = DateTimeImmutable::createFromFormat( + 'Y-m-d\TH:i', + $ranking_hidden_at, + new DateTimeZone('Asia/Tokyo'), + ); + if ($ranking_hidden_at === false) { + $errors['ranking_hidden_at'] = 'ランキングが非表示になる日時の形式が不正です'; + } else { + $ranking_hidden_at = $ranking_hidden_at->setTimezone(new DateTimeZone('UTC')); + } + } + if (!$ranking_hidden_at instanceof DateTimeImmutable) { + $ranking_hidden_at = new DateTimeImmutable('now', new DateTimeZone('UTC')); // dummy + } + if ($finished_at === '') { + $errors['finished_at'] = '終了日時は必須です'; + } else { + $finished_at = DateTimeImmutable::createFromFormat( + 'Y-m-d\TH:i', + $finished_at, + new DateTimeZone('Asia/Tokyo'), + ); + if ($finished_at === false) { + $errors['finished_at'] = '終了日時の形式が不正です'; + } else { + $finished_at = $finished_at->setTimezone(new DateTimeZone('UTC')); + } + } + if (!$finished_at instanceof DateTimeImmutable) { + $finished_at = new DateTimeImmutable('now', new DateTimeZone('UTC')); // dummy + } + + if (0 < count($errors)) { + $this->state->setErrors($errors); + throw new FormSubmissionFailureException(); + } + + try { + $this->quizRepo->create( + $title, + $slug, + $description, + $example_code, + $birdie_code_size === '' ? null : (int)$birdie_code_size, + $started_at, + $ranking_hidden_at, + $finished_at, + ); + } catch (EntityValidationException $e) { + $this->state->setErrors($e->toFormErrors()); + throw new FormSubmissionFailureException(previous: $e); + } + } +} diff --git a/services/app/src/Forms/AdminTestcaseEditForm.php b/services/app/src/Forms/AdminTestcaseEditForm.php new file mode 100644 index 0000000..aa87c8a --- /dev/null +++ b/services/app/src/Forms/AdminTestcaseEditForm.php @@ -0,0 +1,122 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\Albatross\Forms; + +use Nsfisis\Albatross\Database\Connection; +use Nsfisis\Albatross\Exceptions\EntityValidationException; +use Nsfisis\Albatross\Form\FormBase; +use Nsfisis\Albatross\Form\FormItem; +use Nsfisis\Albatross\Form\FormState; +use Nsfisis\Albatross\Form\FormSubmissionFailureException; +use Nsfisis\Albatross\Models\Quiz; +use Nsfisis\Albatross\Models\Testcase; +use Nsfisis\Albatross\Repositories\AnswerRepository; +use Nsfisis\Albatross\Repositories\TestcaseExecutionRepository; +use Nsfisis\Albatross\Repositories\TestcaseRepository; +use Slim\Interfaces\RouteParserInterface; + +final class AdminTestcaseEditForm extends FormBase +{ + public function __construct( + ?FormState $state, + private readonly Testcase $testcase, + private readonly Quiz $quiz, + private readonly RouteParserInterface $routeParser, + private readonly AnswerRepository $answerRepo, + private readonly TestcaseRepository $testcaseRepo, + private readonly TestcaseExecutionRepository $testcaseExecutionRepo, + private readonly Connection $conn, + ) { + if (!isset($state)) { + $state = new FormState([ + 'input' => $testcase->input, + 'expected_result' => $testcase->expected_result, + ]); + } + parent::__construct($state); + } + + public function pageTitle(): string + { + return "管理画面 - 問題 #{$this->quiz->quiz_id} - テストケース #{$this->testcase->testcase_id} - 編集"; + } + + public function redirectUrl(): string + { + return $this->routeParser->urlFor('admin_testcase_list', ['qslug' => $this->quiz->slug]); + } + + protected function submitLabel(): string + { + return '保存'; + } + + /** + * @return list<FormItem> + */ + protected function items(): array + { + return [ + new FormItem( + name: 'input', + type: 'textarea', + label: '標準入力', + extra: 'rows="10" cols="80"', + ), + new FormItem( + name: 'expected_result', + type: 'textarea', + label: '期待する出力', + isRequired: true, + extra: 'rows="10" cols="80"', + ), + ]; + } + + /** + * @return array{testcase: Testcase, quiz: Quiz} + */ + public function getRenderContext(): array + { + return [ + 'testcase' => $this->testcase, + 'quiz' => $this->quiz, + ]; + } + + public function submit(): void + { + $input = $this->state->get('input') ?? ''; + $expected_result = $this->state->get('expected_result') ?? ''; + + $errors = []; + if ($expected_result === '') { + $errors['expected_result'] = '期待する出力は必須です'; + } + if (0 < count($errors)) { + $this->state->setErrors($errors); + throw new FormSubmissionFailureException(); + } + + try { + $this->conn->transaction(function () use ($input, $expected_result): void { + $quiz_id = $this->quiz->quiz_id; + + $this->testcaseRepo->update( + testcase_id: $this->testcase->testcase_id, + input: $input, + expected_result: $expected_result, + ); + $this->answerRepo->markAllAsPending($quiz_id); + $this->testcaseExecutionRepo->markAllAsPendingByTestcaseId( + testcase_id: $this->testcase->testcase_id, + ); + }); + } catch (EntityValidationException $e) { + $this->state->setErrors($e->toFormErrors()); + throw new FormSubmissionFailureException(previous: $e); + } + } +} diff --git a/services/app/src/Forms/AdminTestcaseNewForm.php b/services/app/src/Forms/AdminTestcaseNewForm.php new file mode 100644 index 0000000..e925f8c --- /dev/null +++ b/services/app/src/Forms/AdminTestcaseNewForm.php @@ -0,0 +1,107 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\Albatross\Forms; + +use Nsfisis\Albatross\Database\Connection; +use Nsfisis\Albatross\Exceptions\EntityValidationException; +use Nsfisis\Albatross\Form\FormBase; +use Nsfisis\Albatross\Form\FormItem; +use Nsfisis\Albatross\Form\FormState; +use Nsfisis\Albatross\Form\FormSubmissionFailureException; +use Nsfisis\Albatross\Models\Quiz; +use Nsfisis\Albatross\Repositories\AnswerRepository; +use Nsfisis\Albatross\Repositories\TestcaseExecutionRepository; +use Nsfisis\Albatross\Repositories\TestcaseRepository; +use Slim\Interfaces\RouteParserInterface; + +final class AdminTestcaseNewForm extends FormBase +{ + public function __construct( + ?FormState $state, + private readonly Quiz $quiz, + private readonly RouteParserInterface $routeParser, + private readonly AnswerRepository $answerRepo, + private readonly TestcaseRepository $testcaseRepo, + private readonly TestcaseExecutionRepository $testcaseExecutionRepo, + private readonly Connection $conn, + ) { + if (!isset($state)) { + $state = new FormState(); + } + parent::__construct($state); + } + + public function pageTitle(): string + { + return "管理画面 - 問題 #{$this->quiz->quiz_id} - テストケース作成"; + } + + public function redirectUrl(): string + { + return $this->routeParser->urlFor('admin_testcase_list', ['qslug' => $this->quiz->slug]); + } + + protected function submitLabel(): string + { + return '作成'; + } + + /** + * @return list<FormItem> + */ + protected function items(): array + { + return [ + new FormItem( + name: 'input', + type: 'textarea', + label: '標準入力', + extra: 'rows="10" cols="80"', + ), + new FormItem( + name: 'expected_result', + type: 'textarea', + label: '期待する出力', + isRequired: true, + extra: 'rows="10" cols="80"', + ), + ]; + } + + public function submit(): void + { + $input = $this->state->get('input') ?? ''; + $expected_result = $this->state->get('expected_result') ?? ''; + + $errors = []; + if ($expected_result === '') { + $errors['expected_result'] = '期待する出力は必須です'; + } + if (0 < count($errors)) { + $this->state->setErrors($errors); + throw new FormSubmissionFailureException(); + } + + try { + $this->conn->transaction(function () use ($input, $expected_result): void { + $quiz_id = $this->quiz->quiz_id; + + $testcase_id = $this->testcaseRepo->create( + quiz_id: $quiz_id, + input: $input, + expected_result: $expected_result, + ); + $this->answerRepo->markAllAsPending($quiz_id); + $this->testcaseExecutionRepo->enqueueForAllAnswers( + quiz_id: $quiz_id, + testcase_id: $testcase_id, + ); + }); + } catch (EntityValidationException $e) { + $this->state->setErrors($e->toFormErrors()); + throw new FormSubmissionFailureException(previous: $e); + } + } +} diff --git a/services/app/src/Forms/AdminUserEditForm.php b/services/app/src/Forms/AdminUserEditForm.php new file mode 100644 index 0000000..1a17f6c --- /dev/null +++ b/services/app/src/Forms/AdminUserEditForm.php @@ -0,0 +1,85 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\Albatross\Forms; + +use Nsfisis\Albatross\Form\FormBase; +use Nsfisis\Albatross\Form\FormItem; +use Nsfisis\Albatross\Form\FormState; +use Nsfisis\Albatross\Models\User; +use Nsfisis\Albatross\Repositories\UserRepository; +use Slim\Interfaces\RouteParserInterface; + +final class AdminUserEditForm extends FormBase +{ + public function __construct( + ?FormState $state, + private readonly User $user, + private readonly RouteParserInterface $routeParser, + private readonly UserRepository $userRepo, + ) { + if (!isset($state)) { + $state = new FormState([ + 'username' => $user->username, + 'is_admin' => $user->is_admin ? 'on' : '', + ]); + } + parent::__construct($state); + } + + public function pageTitle(): string + { + return "管理画面 - ユーザ {$this->user->username} 編集"; + } + + public function redirectUrl(): string + { + return $this->routeParser->urlFor('admin_user_list'); + } + + protected function submitLabel(): string + { + return '保存'; + } + + /** + * @return list<FormItem> + */ + protected function items(): array + { + return [ + new FormItem( + name: 'username', + type: 'text', + label: 'ユーザ名', + isDisabled: true, + ), + new FormItem( + name: 'is_admin', + type: 'checkbox', + label: '管理者', + ), + ]; + } + + /** + * @return array{user: User} + */ + public function getRenderContext(): array + { + return [ + 'user' => $this->user, + ]; + } + + public function submit(): void + { + $is_admin = $this->state->get('is_admin') === 'on'; + + $this->userRepo->update( + $this->user->user_id, + is_admin: $is_admin, + ); + } +} diff --git a/services/app/src/Forms/AnswerNewForm.php b/services/app/src/Forms/AnswerNewForm.php new file mode 100644 index 0000000..a07a172 --- /dev/null +++ b/services/app/src/Forms/AnswerNewForm.php @@ -0,0 +1,116 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\Albatross\Forms; + +use Nsfisis\Albatross\Database\Connection; +use Nsfisis\Albatross\Exceptions\EntityValidationException; +use Nsfisis\Albatross\Form\FormBase; +use Nsfisis\Albatross\Form\FormItem; +use Nsfisis\Albatross\Form\FormState; +use Nsfisis\Albatross\Form\FormSubmissionFailureException; +use Nsfisis\Albatross\Models\Answer; +use Nsfisis\Albatross\Models\Quiz; +use Nsfisis\Albatross\Models\User; +use Nsfisis\Albatross\Repositories\AnswerRepository; +use Nsfisis\Albatross\Repositories\TestcaseExecutionRepository; +use Slim\Interfaces\RouteParserInterface; + +final class AnswerNewForm extends FormBase +{ + private ?Answer $answer = null; + + public function __construct( + ?FormState $state, + private readonly User $currentUser, + private readonly Quiz $quiz, + private readonly RouteParserInterface $routeParser, + private readonly AnswerRepository $answerRepo, + private readonly TestcaseExecutionRepository $testcaseExecutionRepo, + private readonly Connection $conn, + ) { + if (!isset($state)) { + $state = new FormState(); + } + parent::__construct($state); + } + + public function pageTitle(): string + { + return "問題 #{$this->quiz->quiz_id} - 提出"; + } + + public function redirectUrl(): string + { + $answer = $this->answer; + assert(isset($answer)); + return $this->routeParser->urlFor( + 'answer_view', + ['qslug' => $this->quiz->slug, 'anum' => "$answer->answer_number"], + ); + } + + protected function submitLabel(): string + { + return '投稿'; + } + + /** + * @return list<FormItem> + */ + protected function items(): array + { + return [ + new FormItem( + name: 'code', + type: 'textarea', + label: 'コード', + isRequired: true, + extra: 'rows="3" cols="80"', + ), + ]; + } + + /** + * @return array{quiz: Quiz, is_closed: bool} + */ + public function getRenderContext(): array + { + return [ + 'quiz' => $this->quiz, + 'is_closed' => $this->quiz->isClosedToAnswer(), + ]; + } + + public function submit(): void + { + if ($this->quiz->isClosedToAnswer()) { + $this->state->setErrors(['general' => 'この問題の回答は締め切られました']); + throw new FormSubmissionFailureException(); + } + + $code = $this->state->get('code') ?? ''; + + try { + $answer_id = $this->conn->transaction(function () use ($code) { + $answer_id = $this->answerRepo->create( + $this->quiz->quiz_id, + $this->currentUser->user_id, + $code, + ); + $this->testcaseExecutionRepo->enqueueForSingleAnswer( + $answer_id, + $this->quiz->quiz_id, + ); + return $answer_id; + }); + } catch (EntityValidationException $e) { + $this->state->setErrors($e->toFormErrors()); + throw new FormSubmissionFailureException(previous: $e); + } + $answer = $this->answerRepo->findById($answer_id); + assert(isset($answer)); + $this->answer = $answer; + } +} diff --git a/services/app/src/Forms/LoginForm.php b/services/app/src/Forms/LoginForm.php new file mode 100644 index 0000000..26f6ad1 --- /dev/null +++ b/services/app/src/Forms/LoginForm.php @@ -0,0 +1,116 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\Albatross\Forms; + +use Nsfisis\Albatross\Auth\AuthenticationResult; +use Nsfisis\Albatross\Auth\AuthProviderInterface; +use Nsfisis\Albatross\Exceptions\EntityValidationException; +use Nsfisis\Albatross\Form\FormBase; +use Nsfisis\Albatross\Form\FormItem; +use Nsfisis\Albatross\Form\FormState; +use Nsfisis\Albatross\Form\FormSubmissionFailureException; +use Nsfisis\Albatross\Repositories\UserRepository; +use Slim\Interfaces\RouteParserInterface; + +final class LoginForm extends FormBase +{ + public function __construct( + ?FormState $state, + private readonly ?string $destination, + private readonly RouteParserInterface $routeParser, + private readonly UserRepository $userRepo, + private readonly AuthProviderInterface $authProvider, + ) { + if (!isset($state)) { + $state = new FormState(); + } + parent::__construct($state); + } + + public function pageTitle(): string + { + return 'ログイン'; + } + + public function redirectUrl(): string + { + return $this->destination ?? $this->routeParser->urlFor('quiz_list'); + } + + protected function submitLabel(): string + { + return 'ログイン'; + } + + /** + * @return list<FormItem> + */ + protected function items(): array + { + return [ + new FormItem( + name: 'username', + type: 'text', + label: 'ユーザ名', + isRequired: true, + ), + new FormItem( + name: 'password', + type: 'password', + label: 'パスワード', + isRequired: true, + ), + ]; + } + + public function submit(): void + { + $username = $this->state->get('username') ?? ''; + $password = $this->state->get('password') ?? ''; + + $this->validate($username, $password); + + $authResult = $this->authProvider->login($username, $password); + if ($authResult === AuthenticationResult::InvalidCredentials) { + $this->state->setErrors(['general' => 'ユーザ名またはパスワードが異なります']); + throw new FormSubmissionFailureException(code: 403); + } elseif ($authResult === AuthenticationResult::InvalidJson || $authResult === AuthenticationResult::UnknownError) { + throw new FormSubmissionFailureException(code: 500); + } else { + $user = $this->userRepo->findByUsername($username); + if ($user === null) { + try { + $user_id = $this->userRepo->create( + $username, + is_admin: true, // TODO + ); + } catch (EntityValidationException $e) { + $this->state->setErrors($e->toFormErrors()); + throw new FormSubmissionFailureException(previous: $e); + } + $_SESSION['user_id'] = $user_id; + } else { + $_SESSION['user_id'] = $user->user_id; + } + } + } + + private function validate(string $username, string $password): void + { + $errors = []; + if (strlen($username) < 1) { + $errors['username'] = 'ユーザ名は必須です'; + } + + if (strlen($password) < 1) { + $errors['password'] = 'パスワードは必須です'; + } + + if (count($errors) > 0) { + $this->state->setErrors($errors); + throw new FormSubmissionFailureException(code: 400); + } + } +} |
