From 72c266a29716b431a917d03d736aa2e8d26337ee Mon Sep 17 00:00:00 2001 From: "sosuke.iwabuchi" Date: Wed, 20 Sep 2023 13:43:34 +0900 Subject: [PATCH] =?UTF-8?q?Email=E5=A4=89=E6=9B=B4=E6=89=8B=E7=B6=9A?= =?UTF-8?q?=E3=81=8D=E3=80=80=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Console/Commands/TestEmail.php | 67 +++++++ app/Email/BaseEmailer.php | 16 -- app/Email/Guests/ChangeEmailStart.php | 35 ++++ app/Email/Members/Members.php | 2 +- app/Files/BaseFile.php | 182 ++++++++++++++++++ app/Files/CsvFile.php | 72 +++++++ app/Files/Image.php | 22 +++ app/Files/TmpFile.php | 130 +++++++++++++ .../Customer/ChangeEmailStartController.php | 40 ++++ .../Web/Customer/ChangeEmailStartParams.php | 19 ++ .../Customer/ChangeEmailVerifyController.php | 44 +++++ .../Web/Customer/ChangeEmailVerifyParams.php | 19 ++ app/Kintone/KintoneAccess.php | 17 ++ app/Logic/EmailChangeManager.php | 109 +++++++++++ app/Logic/EmailManager.php | 126 ++++++++++++ app/Models/Email.php | 24 +++ app/Models/EmailAttachment.php | 17 ++ app/Models/EmailChangeToken.php | 37 ++++ app/Models/EmailHistory.php | 12 ++ app/Models/Feature/UserId.php | 13 ++ config/mail.php | 2 + .../2023_04_13_100116_create_jobs_table.php | 32 +++ ...31600_create_email_change_tokens_table.php | 39 ++++ .../2023_09_14_151800_create_emails_table.php | 54 ++++++ ..._153300_create_email_attachments_table.php | 46 +++++ docker-compose.yml | 2 +- docker/8.2/Dockerfile | 62 ++++++ docker/8.2/php.ini | 4 + docker/8.2/start-container | 17 ++ docker/8.2/supervisord.conf | 68 +++++++ resources/views/emails/free_text.blade.php | 1 + .../guests/change_email_start.blade.php | 6 + .../views/emails/layouts/guest.blade.php | 11 ++ .../views/emails/layouts/member.blade.php | 11 ++ resources/views/emails/test.blade.php | 1 + routes/api.php | 3 + 36 files changed, 1344 insertions(+), 18 deletions(-) create mode 100644 app/Console/Commands/TestEmail.php create mode 100644 app/Email/Guests/ChangeEmailStart.php create mode 100644 app/Files/BaseFile.php create mode 100644 app/Files/CsvFile.php create mode 100644 app/Files/Image.php create mode 100644 app/Files/TmpFile.php create mode 100644 app/Http/Controllers/Web/Customer/ChangeEmailStartController.php create mode 100644 app/Http/Controllers/Web/Customer/ChangeEmailStartParams.php create mode 100644 app/Http/Controllers/Web/Customer/ChangeEmailVerifyController.php create mode 100644 app/Http/Controllers/Web/Customer/ChangeEmailVerifyParams.php create mode 100644 app/Logic/EmailChangeManager.php create mode 100644 app/Logic/EmailManager.php create mode 100644 app/Models/Email.php create mode 100644 app/Models/EmailAttachment.php create mode 100644 app/Models/EmailChangeToken.php create mode 100644 app/Models/EmailHistory.php create mode 100644 app/Models/Feature/UserId.php create mode 100644 database/migrations/2023_04_13_100116_create_jobs_table.php create mode 100644 database/migrations/2023_09_14_131600_create_email_change_tokens_table.php create mode 100644 database/migrations/2023_09_14_151800_create_emails_table.php create mode 100644 database/migrations/2023_09_14_153300_create_email_attachments_table.php create mode 100644 docker/8.2/Dockerfile create mode 100644 docker/8.2/php.ini create mode 100644 docker/8.2/start-container create mode 100644 docker/8.2/supervisord.conf create mode 100644 resources/views/emails/free_text.blade.php create mode 100644 resources/views/emails/guests/change_email_start.blade.php create mode 100644 resources/views/emails/layouts/guest.blade.php create mode 100644 resources/views/emails/layouts/member.blade.php create mode 100644 resources/views/emails/test.blade.php diff --git a/app/Console/Commands/TestEmail.php b/app/Console/Commands/TestEmail.php new file mode 100644 index 0000000..71ef4f0 --- /dev/null +++ b/app/Console/Commands/TestEmail.php @@ -0,0 +1,67 @@ +argument('email'); + + $file = new TmpFile(); + $file->put("iwabuchi text"); + + + $mailer = new Test(); + $mailer->setEmail($email); + + $manager = new EmailManager($mailer); + $manager + ->attach($file->getFullPath(), "test.txt", "text/plain") + ->confirm(); + + return self::RESULTCODE_SUCCESS; + } +} diff --git a/app/Email/BaseEmailer.php b/app/Email/BaseEmailer.php index 326b40d..76de0f3 100644 --- a/app/Email/BaseEmailer.php +++ b/app/Email/BaseEmailer.php @@ -26,8 +26,6 @@ abstract class BaseEmailer extends Mailable protected User|null $__user = null; protected string $__email = ""; protected string|null $__userId = null; - protected string|null $__contractId = null; - protected string|null $__receiptIssuingOrderId = null; /** * 添付ファイル @@ -61,18 +59,6 @@ abstract class BaseEmailer extends Mailable return $this; } - public function setContractId(string $contractId) - { - $this->__contractId = $contractId; - return $this; - } - - public function setReceiptIssuingOrderId(string $receiptIssuingOrderId) - { - $this->__receiptIssuingOrderId = $receiptIssuingOrderId; - return $this; - } - public function build() { $this->text($this->getTemplateName()) @@ -174,9 +160,7 @@ abstract class BaseEmailer extends Mailable $model->type = get_class($this); $model->email = $this->__email; - $model->contract_id = $this->__contractId; $model->user_id = $this->__userId; - $model->receipt_issuing_order_id = $this->__receiptIssuingOrderId; return $model; } diff --git a/app/Email/Guests/ChangeEmailStart.php b/app/Email/Guests/ChangeEmailStart.php new file mode 100644 index 0000000..9b4eb04 --- /dev/null +++ b/app/Email/Guests/ChangeEmailStart.php @@ -0,0 +1,35 @@ + $this->getVerifyUrl(), + ]; + } + + private function getVerifyUrl(): string + { + return $this->getAppUrl(['change', 'email', 'verify', $this->token->token]); + } +} diff --git a/app/Email/Members/Members.php b/app/Email/Members/Members.php index 0d8c610..e4df79d 100644 --- a/app/Email/Members/Members.php +++ b/app/Email/Members/Members.php @@ -8,7 +8,7 @@ abstract class Members extends BaseEmailer { public function getParams(): array { - return array_merge($this->getGuestParams(), []); + return array_merge($this->getMemberParams(), []); } abstract public function getMemberParams(): array; diff --git a/app/Files/BaseFile.php b/app/Files/BaseFile.php new file mode 100644 index 0000000..2e771d0 --- /dev/null +++ b/app/Files/BaseFile.php @@ -0,0 +1,182 @@ +|Collection|BaseFile $files + * @return void + */ + public static function commitAll(array|Collection|BaseFile $files) + { + if (is_array($files) || $files instanceof Collection) { + foreach ($files as $file) { + $file->commit(); + } + } else { + $files->commit(); + } + } + + + public function __construct(UploadedFile $file = null) + { + $this->file = $file; + } + + /** + * 変更後、コミットしていない場合は削除する + */ + public function __destruct() + { + if (!$this->commit && $this->updatedAt !== null) { + $this->delete(); + } + } + + /** + * コミット + * + * @param boolean $commit + * @return void + */ + public function commit($commit = true) + { + $this->commit = $commit; + } + + /** + * ファイルパスを取得する disk.rootからの相対パス + * + * @return string + */ + public function getFilepath(): string + { + return $this->getDir() . "/" . $this->getFilename(); + } + + /** + * ファイル取得 + * + * @return string|bool + */ + public function get(): string|bool + { + if ($this->exists()) { + return Crypt::decryptString(Storage::get($this->getFilepath())); + } + return false; + } + + /** + * ファイルの存在確認 + * + * @return boolean + */ + public function exists(): bool + { + return Storage::exists($this->getFilepath()); + } + + /** + * ファイル削除 + * + * @return boolean 成功可否 + */ + public function delete(): bool + { + if ($this->exists()) { + return Storage::delete($this->getFilepath()); + } + return true; + } + + /** + * アップロードファイルの保存 + * + * @param UploadedFile $file + * @param Carbon|null|null $updatedAt + * @return boolean + */ + public function store(Carbon|null $timestamp = null): bool + { + + if ($this->file === null) return false; + + $this->updatedAt = $timestamp ?? Now::get(); + $contents = Crypt::encryptString($this->file->get()); + + $ret = Storage::put($this->getDir() . DIRECTORY_SEPARATOR . $this->getFilename(), $contents); + + if ($ret === false) { + return false; + } + + + //DBへの登録 + try { + $ret = $this->onUpload($timestamp ?? Now::get()); + if (!$ret) { + $this->delete(); + } + } catch (Exception $e) { + $this->delete(); + throw $e; + } + + + return $ret; + } + + public function toImageStr() + { + return (new Image($this))->__toString(); + } +} diff --git a/app/Files/CsvFile.php b/app/Files/CsvFile.php new file mode 100644 index 0000000..2b9c6d1 --- /dev/null +++ b/app/Files/CsvFile.php @@ -0,0 +1,72 @@ +addLine($headers); + } + } + + public function addLine(array|Collection $row, array|null $sortDef = null) + { + if ($sortDef !== null) { + $row = $this->sortColumn($sortDef, $row); + } + + $str = ""; + foreach ($row as $col => $val) { + if ($str !== "") { + $str .= ","; + } + + $str .= $val; + } + + if ($this->encode === static::ENCODE_SJIS) { + $str = $this->toSjis($str); + } + + $this->append($str); + } + + private function toSjis(string $source): string + { + return mb_convert_encoding($source, "SJIS", "UTF8"); + } + + private function sortColumn(array $sortDef, $data): array + { + $ele = []; + $notFound = Str::uuid(); + foreach ($sortDef as $def) { + $ret = data_get($data, $def, $notFound); + if ($ret === $notFound) { + throw new LogicException("存在しない項目:" . $def); + } + $ele[] = $ret; + } + return $ele; + } +} diff --git a/app/Files/Image.php b/app/Files/Image.php new file mode 100644 index 0000000..125fad6 --- /dev/null +++ b/app/Files/Image.php @@ -0,0 +1,22 @@ +binary = $file->get(); + $this->mimetype = $file->getMimetype(); + } + + public function __toString() + { + return sprintf("data:%s;base64,%s", $this->mimetype, base64_encode($this->binary)); + } +} diff --git a/app/Files/TmpFile.php b/app/Files/TmpFile.php new file mode 100644 index 0000000..a1ae4d0 --- /dev/null +++ b/app/Files/TmpFile.php @@ -0,0 +1,130 @@ +exists()) { + throw new FileNotFoundException("ファイルが存在しません:" . $file->getFullPath()); + } + return $file; + } + + + + protected string $uuid; + + + public function __construct(?string $id = null) + { + if ($id === null) { + $this->uuid = Str::uuid(); + } else { + $this->uuid = $id; + } + } + + public function __destruct() + { + // 消し忘れ防止のため、削除を予約しておく + if ($this->exists()) { + $lifeTimeMin = config("filesystems.tmpFile.lifetime", 60); + $this->delete(DateUtil::now()->addMinutes($lifeTimeMin)); + } + } + + protected function getFileTypeName() + { + return "tmp"; + } + + protected function getFileExtension(): string + { + return "tmp"; + } + + final protected function getFileName(): string + { + return sprintf("%s_%s.%s", $this->getFileTypeName(), $this->uuid, $this->getFileExtension()); + } + + public function getId(): string + { + return $this->uuid; + } + + public function getPath() + { + return implode( + "/", + [ + self::BASE_DIR, + ...static::DIR + ] + ) . "/" . $this->getFileName(); + } + + public function getFullPath() + { + return Storage::path($this->getPath()); + } + + public function put(string $content) + { + Storage::put($this->getPath(), $content); + } + + public function get() + { + return Storage::get($this->getPath()); + } + + public function append(string $content) + { + Storage::append($this->getPath(), $content); + } + + public function download(string $name = "download") + { + return response()->download($this->getFullPath(), $name)->deleteFileAfterSend(); + } + + public function exists() + { + return Storage::exists($this->getPath()); + } + + public function delete(?Carbon $delay = null): void + { + if ($delay === null) { + $ret = Storage::delete($this->getPath()); + if ($ret) info(sprintf("ファイル削除:%s ", $this->getFullPath())); + return; + } else { + DeleteFile::dispatch($this) + ->delay($delay); + return; + } + } +} diff --git a/app/Http/Controllers/Web/Customer/ChangeEmailStartController.php b/app/Http/Controllers/Web/Customer/ChangeEmailStartController.php new file mode 100644 index 0000000..b531789 --- /dev/null +++ b/app/Http/Controllers/Web/Customer/ChangeEmailStartController.php @@ -0,0 +1,40 @@ +middleware('auth:sanctum'); + } + + protected function run(Request $request): JsonResponse + { + $param = $this->param; + + $this->manager->setUser(Auth::user()) + ->generate($param->newEmail); + + return $this->successResponse(); + } +} diff --git a/app/Http/Controllers/Web/Customer/ChangeEmailStartParams.php b/app/Http/Controllers/Web/Customer/ChangeEmailStartParams.php new file mode 100644 index 0000000..5c715af --- /dev/null +++ b/app/Http/Controllers/Web/Customer/ChangeEmailStartParams.php @@ -0,0 +1,19 @@ + $this->str([...Rule::email()]), + ]; + } +} diff --git a/app/Http/Controllers/Web/Customer/ChangeEmailVerifyController.php b/app/Http/Controllers/Web/Customer/ChangeEmailVerifyController.php new file mode 100644 index 0000000..26a55ba --- /dev/null +++ b/app/Http/Controllers/Web/Customer/ChangeEmailVerifyController.php @@ -0,0 +1,44 @@ +middleware('auth:sanctum'); + } + + protected function run(Request $request): JsonResponse + { + $param = $this->param; + + $ret = $this->manager + ->verify($param->token); + + if (!$ret) { + $this->failedResponse(); + } + + return $this->successResponse(); + } +} diff --git a/app/Http/Controllers/Web/Customer/ChangeEmailVerifyParams.php b/app/Http/Controllers/Web/Customer/ChangeEmailVerifyParams.php new file mode 100644 index 0000000..4ec3794 --- /dev/null +++ b/app/Http/Controllers/Web/Customer/ChangeEmailVerifyParams.php @@ -0,0 +1,19 @@ + $this->str(), + ]; + } +} diff --git a/app/Kintone/KintoneAccess.php b/app/Kintone/KintoneAccess.php index 1ec614d..66af4ff 100644 --- a/app/Kintone/KintoneAccess.php +++ b/app/Kintone/KintoneAccess.php @@ -2,9 +2,11 @@ namespace App\Kintone; +use App\Exceptions\AppCommonException; use App\Exceptions\ConfigException; use App\Kintone\Models\KintoneModel; use Exception; +use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Http\UploadedFile; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Http; @@ -165,6 +167,21 @@ class KintoneAccess return $ret; } + /** + * @param KintoneRecordQuery|null|null $query + * @return TValue + */ + public function first(KintoneRecordQuery|null $query = null) + { + $list = $this->some($query); + if ($list->count() !== 1) { + throw new ModelNotFoundException(sprintf("モデル取得数エラー %s count:%d", static::class, $list->count())); + } + return $list->first(); + } + + + /** * @param TValue $model * @return void diff --git a/app/Logic/EmailChangeManager.php b/app/Logic/EmailChangeManager.php new file mode 100644 index 0000000..edafa87 --- /dev/null +++ b/app/Logic/EmailChangeManager.php @@ -0,0 +1,109 @@ +user = $user; + $this->model = EmailChangeToken::whereUserId($user->id)->first(); + return $this; + } + + public function generate(string $newEmail): EmailChangeToken + { + if ($this->user === null) { + throw new LogicException("User不正"); + } + + // 重複チェック + if (!$this->checkDuplication($newEmail)) { + throw new GeneralErrorMessageException("すでに登録されているEmailです"); + } + + if ($this->model === null) { + $this->model = new EmailChangeToken(); + } + + $this->model->user_id = $this->user->id; + $this->model->token = Str::uuid(); + $this->model->new_email = $newEmail; + $this->setExpires(); + + $this->model->save(); + + + // メール送信 + $email = (new ChangeEmailStart($this->model)) + ->setEmail($newEmail); + $emailManager = new EmailManager($email); + $emailManager->confirm(); + + return $this->model; + } + + public function verify(string $token) + { + $model = EmailChangeToken::whereToken($token)->firstOrFail(); + + $user = $model->user; + if ($user === null) { + throw new LogicException("User不正"); + } + // 利用者情報の更新 + $user->email = $model->new_email; + $user->save(); + + // KINTONE側の更新 + $access = Customer::getAccess(); + $customer = $access->find($user->kintone_id); + $customer->set(Customer::FIELD_EMAIL, $model->new_email); + $access->update($customer); + + // トークン削除 + $model->delete(); + return true; + } + + /** + * 重複チェック + * + * @param string $newEmail + * @return boolean + */ + private function checkDuplication(string $newEmail): bool + { + return !User::whereEmail($newEmail)->exists() && + !EmailChangeToken::whereNewEmail($newEmail)->expiresIn() + ->whereNot(EmailChangeToken::COL_NAME_USER_ID, $this->user->id) + ->exists(); + } + + private function setExpires() + { + if ($this->model === null) { + throw new LogicException("Model不正"); + } + $this->model->expires_at = DateUtil::now()->addHours(24); + } +} diff --git a/app/Logic/EmailManager.php b/app/Logic/EmailManager.php new file mode 100644 index 0000000..060c407 --- /dev/null +++ b/app/Logic/EmailManager.php @@ -0,0 +1,126 @@ +|null + */ + private ?Collection $attachments = null; + + public function __construct(int|BaseEmailer $param) + { + if (is_numeric($param)) { + $this->model = Email::lockForUpdate()->findOrfail($param); + $this->canSend = $this->model->send_datetime === null; + if (!$this->checkAuth()) throw new AppCommonException("メール権限エラー"); + } else if ($param instanceof BaseEmailer) { + $this->model = $param->makeModel(); + } + } + + public function checkAuth() + { + return true; + } + + public function getEmailId() + { + $this->model->setId(); + return $this->model->id; + } + + public function getTimestamp(): Carbon|null + { + return $this->model->updated_at; + } + + public function create() + { + $this->model->save(); + return []; + } + + public function setSubject(string $subject) + { + if ($this->canSend()) { + $this->model->subject = $subject; + } else { + throw new AppCommonException("送信済みメール編集エラー"); + } + return $this; + } + + public function setContent(string $content) + { + + if ($this->canSend()) { + $this->model->content = $content; + } else { + throw new AppCommonException("送信済みメール編集エラー"); + } + return $this; + } + + public function attach(string $filepath, string $filename, string $mimeType) + { + + if ($this->attachments === null) { + $this->attachments = new Collection(); + } + $attachment = new EmailAttachment(); + $attachment->filepath = $filepath; + $attachment->send_filename = $filename; + $attachment->mime = $mimeType; + + $this->attachments->push($attachment); + + return $this; + } + + public function update() + { + $this->model->save(); + return []; + } + + public function confirm() + { + + $validator = Validator::make(['email' => $this->model->email], [ + 'email' => ['required', 'email:strict,dns'] + ]); + + if ($validator->fails()) { + throw new Exception(sprintf("%s [%s]", $validator->errors()->first(), $this->model->email)); + } + + if ($this->canSend() !== null) { + $this->model->setId(); + ConfirmEvent::dispatch($this->model, $this->attachments); + } else { + throw new AppCommonException("送信済みエラー"); + } + } + + public function canSend() + { + return $this->canSend; + } +} diff --git a/app/Models/Email.php b/app/Models/Email.php new file mode 100644 index 0000000..8c7d0bb --- /dev/null +++ b/app/Models/Email.php @@ -0,0 +1,24 @@ +hasMany(EmailAttachment::class); + } + + protected $casts = [ + self::COL_NAME_SEND_DATETIME => 'datetime', + ]; +} diff --git a/app/Models/EmailAttachment.php b/app/Models/EmailAttachment.php new file mode 100644 index 0000000..2f7d3a8 --- /dev/null +++ b/app/Models/EmailAttachment.php @@ -0,0 +1,17 @@ + 'datetime', + ]; + + public function getHistory(): ?HistoryModel + { + return null; + } + + public function getModelName(): string + { + return "Email変更トークン"; + } + + public function scopeExpiresIn(Builder $query) + { + return $query->where(self::COL_NAME_EXPIRES_AT, '>', DateUtil::now()); + } +} diff --git a/app/Models/EmailHistory.php b/app/Models/EmailHistory.php new file mode 100644 index 0000000..831b416 --- /dev/null +++ b/app/Models/EmailHistory.php @@ -0,0 +1,12 @@ +belongsTo(User::class); + } +} diff --git a/config/mail.php b/config/mail.php index 542d98c..b3851fd 100644 --- a/config/mail.php +++ b/config/mail.php @@ -43,6 +43,8 @@ return [ 'password' => env('MAIL_PASSWORD'), 'timeout' => null, 'local_domain' => env('MAIL_EHLO_DOMAIN'), + 'auth_mode' => null, + 'verify_peer' => false, ], 'ses' => [ diff --git a/database/migrations/2023_04_13_100116_create_jobs_table.php b/database/migrations/2023_04_13_100116_create_jobs_table.php new file mode 100644 index 0000000..6098d9b --- /dev/null +++ b/database/migrations/2023_04_13_100116_create_jobs_table.php @@ -0,0 +1,32 @@ +bigIncrements('id'); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedTinyInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('jobs'); + } +}; diff --git a/database/migrations/2023_09_14_131600_create_email_change_tokens_table.php b/database/migrations/2023_09_14_131600_create_email_change_tokens_table.php new file mode 100644 index 0000000..9f0cae7 --- /dev/null +++ b/database/migrations/2023_09_14_131600_create_email_change_tokens_table.php @@ -0,0 +1,39 @@ +baseColumn(); + + $table->uuid(ColumnName::USER_ID)->unique()->comment('ユーザーID'); + $table->string('new_email')->unique()->comment('新規Email'); + $table->uuid('token')->unique()->comment('トークン'); + $table->string('expires_at')->comment('有効期限'); + + $helper->index(1, [ColumnName::USER_ID]); + $helper->index(2, ['new_email']); + $helper->index(3, ['token']); + $helper->index(4, ['expires_at']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('email_change_tokens'); + } +}; diff --git a/database/migrations/2023_09_14_151800_create_emails_table.php b/database/migrations/2023_09_14_151800_create_emails_table.php new file mode 100644 index 0000000..b83df39 --- /dev/null +++ b/database/migrations/2023_09_14_151800_create_emails_table.php @@ -0,0 +1,54 @@ +schema()); + MigrationHelper::createTable('email_histories', $this->schema()); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('emails'); + Schema::dropIfExists('email_histories'); + } + + private function schema() + { + + return function (Blueprint $table, MigrationHelper $helper) { + $helper->baseColumn() + ->userId(true); + + $table->dateTime("confirm_datetime")->comment("確定時刻")->nullable(); + $table->dateTime('send_datetime')->comment("送信時刻")->nullable(); + $table->string('email')->comment("Email"); + $table->string('subject')->comment("件名"); + $table->text('content')->comment("本文"); + $table->string('type')->comment("Emailタイプ"); + $table->boolean('is_failed')->comment("失敗")->nullable(); + + + $helper->index(1, [ColumnName::USER_ID]); + $helper->index(2, ['email']); + $helper->index(3, ['is_failed']); + }; + } +}; diff --git a/database/migrations/2023_09_14_153300_create_email_attachments_table.php b/database/migrations/2023_09_14_153300_create_email_attachments_table.php new file mode 100644 index 0000000..48060e6 --- /dev/null +++ b/database/migrations/2023_09_14_153300_create_email_attachments_table.php @@ -0,0 +1,46 @@ +schema()); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('email_attachments'); + } + + private function schema() + { + + return function (Blueprint $table, MigrationHelper $helper) { + $helper->baseColumn() + ->emailId(); + + $table->string('filepath')->comment("ファイルパス"); + $table->string('send_filename')->comment("送信ファイル名"); + $table->string('mime')->comment("MIMEタイプ"); + + $helper->index(1, [ColumnName::EMAIL_ID]); + $helper->index(2, [ColumnName::CREATED_AT]); + }; + } +}; diff --git a/docker-compose.yml b/docker-compose.yml index 0a6cfd4..c8f554e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: laravel.test: build: - context: ./vendor/laravel/sail/runtimes/8.2 + context: ./docker/8.2 dockerfile: Dockerfile args: WWWGROUP: '${WWWGROUP}' diff --git a/docker/8.2/Dockerfile b/docker/8.2/Dockerfile new file mode 100644 index 0000000..2473fa3 --- /dev/null +++ b/docker/8.2/Dockerfile @@ -0,0 +1,62 @@ +FROM ubuntu:22.04 + +LABEL maintainer="Taylor Otwell" + +ARG WWWGROUP +ARG NODE_VERSION=18 +ARG POSTGRES_VERSION=14 + +WORKDIR /var/www/html + +ENV DEBIAN_FRONTEND noninteractive +ENV TZ=UTC + +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +RUN apt-get update \ + && apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python2 dnsutils \ + && curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x14aa40ec0831756756d7f66c4f4ea0aae5267a6c' | gpg --dearmor | tee /etc/apt/keyrings/ppa_ondrej_php.gpg > /dev/null \ + && echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu jammy main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \ + && apt-get update \ + && apt-get install -y php8.2-cli php8.2-dev \ + php8.2-pgsql php8.2-sqlite3 php8.2-gd \ + php8.2-curl \ + php8.2-imap php8.2-mysql php8.2-mbstring \ + php8.2-xml php8.2-zip php8.2-bcmath php8.2-soap \ + php8.2-intl php8.2-readline \ + php8.2-ldap \ + php8.2-msgpack php8.2-igbinary php8.2-redis php8.2-swoole \ + php8.2-memcached php8.2-pcov php8.2-xdebug \ + libxrender-dev \ + fontconfig \ + fonts-ipafont \ + fonts-ipaexfont \ + && php -r "readfile('https://getcomposer.org/installer');" | php -- --install-dir=/usr/bin/ --filename=composer \ + && curl -sLS https://deb.nodesource.com/setup_$NODE_VERSION.x | bash - \ + && apt-get install -y nodejs \ + && npm install -g npm \ + && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /etc/apt/keyrings/yarn.gpg >/dev/null \ + && echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ + && curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/keyrings/pgdg.gpg >/dev/null \ + && echo "deb [signed-by=/etc/apt/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt jammy-pgdg main" > /etc/apt/sources.list.d/pgdg.list \ + && apt-get update \ + && apt-get install -y yarn \ + && apt-get install -y mysql-client \ + && apt-get install -y postgresql-client-$POSTGRES_VERSION \ + && apt-get -y autoremove \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.2 + +RUN groupadd --force -g $WWWGROUP sail +RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail + +COPY start-container /usr/local/bin/start-container +COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf +COPY php.ini /etc/php/8.2/cli/conf.d/99-sail.ini +RUN chmod +x /usr/local/bin/start-container + +EXPOSE 8000 + +ENTRYPOINT ["start-container"] diff --git a/docker/8.2/php.ini b/docker/8.2/php.ini new file mode 100644 index 0000000..66d04d5 --- /dev/null +++ b/docker/8.2/php.ini @@ -0,0 +1,4 @@ +[PHP] +post_max_size = 100M +upload_max_filesize = 100M +variables_order = EGPCS diff --git a/docker/8.2/start-container b/docker/8.2/start-container new file mode 100644 index 0000000..b864399 --- /dev/null +++ b/docker/8.2/start-container @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +if [ ! -z "$WWWUSER" ]; then + usermod -u $WWWUSER sail +fi + +if [ ! -d /.composer ]; then + mkdir /.composer +fi + +chmod -R ugo+rw /.composer + +if [ $# -gt 0 ]; then + exec gosu $WWWUSER "$@" +else + exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf +fi diff --git a/docker/8.2/supervisord.conf b/docker/8.2/supervisord.conf new file mode 100644 index 0000000..48b0e54 --- /dev/null +++ b/docker/8.2/supervisord.conf @@ -0,0 +1,68 @@ +[supervisord] +nodaemon=true +user=root +logfile=/var/log/supervisor/supervisord.log +pidfile=/var/run/supervisord.pid + +[program:php] +command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80 +user=sail +environment=LARAVEL_SAIL="1" +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +[group:laravel-workers] +programs=laravel-email-worker,laravel-job-worker # カンマでプログラムを指定する +process_name=%(program_name)s_%(process_num)02d +autostart=true +autorestart=true +stopasgroup=true +startsecs=0 +killasgroup=true +user=sail +numprocs=1 +redirect_stderr=true +stopwaitsecs=100 +stdout_logfile_maxbytes=1000000 +stderr_logfile_maxbytes=1000000 +stdout_logfile_backups=3 +stderr_logfile_backups=3 + + +[program:laravel-email-worker] +command=/usr/bin/php /var/www/html/artisan queue:work --queue=email --max-time=60 --tries=0 +stdout_logfile=/var/www/html/storage/logs/laravel-email-worker.log +stderr_logfile=/var/www/html/storage/logs/laravel-email-worker.error.log +autostart=true +autorestart=true +stopasgroup=true +startsecs=0 +killasgroup=true +user=sail +numprocs=1 +redirect_stderr=true +stopwaitsecs=100 +stdout_logfile_maxbytes=1MB +stderr_logfile_maxbytes=1MB +stdout_logfile_backups=3 +stderr_logfile_backups=3 + +[program:laravel-job-worker] +command=/usr/bin/php /var/www/html/artisan queue:work --queue=job --max-time=60 --tries=0 +stdout_logfile=/var/www/html/storage/logs/laravel-job-worker.log +stderr_logfile=/var/www/html/storage/logs/laravel-job-worker.error.log +autostart=true +autorestart=true +stopasgroup=true +startsecs=0 +killasgroup=true +user=sail +numprocs=1 +redirect_stderr=true +stopwaitsecs=100 +stdout_logfile_maxbytes=1MB +stderr_logfile_maxbytes=1MB +stdout_logfile_backups=3 +stderr_logfile_backups=3 \ No newline at end of file diff --git a/resources/views/emails/free_text.blade.php b/resources/views/emails/free_text.blade.php new file mode 100644 index 0000000..3b153ec --- /dev/null +++ b/resources/views/emails/free_text.blade.php @@ -0,0 +1 @@ +{{ $contents }} \ No newline at end of file diff --git a/resources/views/emails/guests/change_email_start.blade.php b/resources/views/emails/guests/change_email_start.blade.php new file mode 100644 index 0000000..c0fd73f --- /dev/null +++ b/resources/views/emails/guests/change_email_start.blade.php @@ -0,0 +1,6 @@ +@extends('emails.layouts.guest') + +@section('contents') +こちらのURLにアクセスし、メールアドレスの変更手続きを進めてください。 +{{ $url }} +@endsection \ No newline at end of file diff --git a/resources/views/emails/layouts/guest.blade.php b/resources/views/emails/layouts/guest.blade.php new file mode 100644 index 0000000..9ce4b15 --- /dev/null +++ b/resources/views/emails/layouts/guest.blade.php @@ -0,0 +1,11 @@ +ご利用ありがとうございます。 +MyPageです。 + +@yield('contents') + + +※このメールをお送りしているアドレスは、送信専用となっており、返信いただいてもご回答いたしかねます。 +※このメールにお心当たりのない方は、お手数ですが削除いただきますようお願いいたします。 + +--------------------------------------------------------------------------------------- +MyPage \ No newline at end of file diff --git a/resources/views/emails/layouts/member.blade.php b/resources/views/emails/layouts/member.blade.php new file mode 100644 index 0000000..c610e35 --- /dev/null +++ b/resources/views/emails/layouts/member.blade.php @@ -0,0 +1,11 @@ +ご利用ありがとうございます。 +EasyReceiptです。 + +@yield('contents') + + +※このメールをお送りしているアドレスは、送信専用となっており、返信いただいてもご回答いたしかねます。 +※このメールにお心当たりのない方は、お手数ですが削除いただきますようお願いいたします。 + +--------------------------------------------------------------------------------------- +EasyReceipt \ No newline at end of file diff --git a/resources/views/emails/test.blade.php b/resources/views/emails/test.blade.php new file mode 100644 index 0000000..5b697b9 --- /dev/null +++ b/resources/views/emails/test.blade.php @@ -0,0 +1 @@ +テストメールです \ No newline at end of file diff --git a/routes/api.php b/routes/api.php index 7a382d1..4d13a7e 100644 --- a/routes/api.php +++ b/routes/api.php @@ -30,3 +30,6 @@ RouteHelper::get('/faq/genres', App\Http\Controllers\Web\FAQ\FAQGenresController RouteHelper::post('/ask', App\Http\Controllers\Web\FAQ\AskController::class); RouteHelper::post('/upload/student-license-images', App\Http\Controllers\Web\Customer\UploadStudentLicenseImagesController::class); RouteHelper::post('/upload/other-license-images', App\Http\Controllers\Web\Customer\UploadOtherLicenseImagesController::class); + +RouteHelper::post('/email/change/start', App\Http\Controllers\Web\Customer\ChangeEmailStartController::class); +RouteHelper::post('/email/change/verify', App\Http\Controllers\Web\Customer\ChangeEmailVerifyController::class);