| @@ -0,0 +1,67 @@ | |||||
| <?php | |||||
| namespace App\Console\Commands; | |||||
| use App\Files\TmpFile; | |||||
| use App\Logic\EmailManager; | |||||
| use App\Email\Test; | |||||
| class TestEmail extends BaseCommand | |||||
| { | |||||
| const COMMAND = "mail:test {email}"; | |||||
| /** | |||||
| * The name and signature of the console command. | |||||
| * | |||||
| * @var string | |||||
| */ | |||||
| protected $signature = self::COMMAND; | |||||
| /** | |||||
| * The console command description. | |||||
| * | |||||
| * @var string | |||||
| */ | |||||
| protected $description = 'テストメールを送信する(キュー登録)'; | |||||
| static public function getCommand() | |||||
| { | |||||
| return self::COMMAND; | |||||
| } | |||||
| /** | |||||
| * Create a new command instance. | |||||
| * | |||||
| * @return void | |||||
| */ | |||||
| public function __construct() | |||||
| { | |||||
| parent::__construct(); | |||||
| } | |||||
| /** | |||||
| * Execute the console command. | |||||
| * | |||||
| * @return int | |||||
| */ | |||||
| public function service(): int | |||||
| { | |||||
| $email = $this->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; | |||||
| } | |||||
| } | |||||
| @@ -26,8 +26,6 @@ abstract class BaseEmailer extends Mailable | |||||
| protected User|null $__user = null; | protected User|null $__user = null; | ||||
| protected string $__email = ""; | protected string $__email = ""; | ||||
| protected string|null $__userId = null; | 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; | 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() | public function build() | ||||
| { | { | ||||
| $this->text($this->getTemplateName()) | $this->text($this->getTemplateName()) | ||||
| @@ -174,9 +160,7 @@ abstract class BaseEmailer extends Mailable | |||||
| $model->type = get_class($this); | $model->type = get_class($this); | ||||
| $model->email = $this->__email; | $model->email = $this->__email; | ||||
| $model->contract_id = $this->__contractId; | |||||
| $model->user_id = $this->__userId; | $model->user_id = $this->__userId; | ||||
| $model->receipt_issuing_order_id = $this->__receiptIssuingOrderId; | |||||
| return $model; | return $model; | ||||
| } | } | ||||
| @@ -0,0 +1,35 @@ | |||||
| <?php | |||||
| namespace App\Email\Guests; | |||||
| use App\Models\EmailChangeToken; | |||||
| class ChangeEmailStart extends Guest | |||||
| { | |||||
| public function __construct(private EmailChangeToken $token) | |||||
| { | |||||
| } | |||||
| public function getTemplateName(): string | |||||
| { | |||||
| return 'emails.guests.change_email_start'; | |||||
| } | |||||
| public function getSubject(): string | |||||
| { | |||||
| return "Emailアドレス変更手続きのお知らせ"; | |||||
| } | |||||
| public function getGuestParams(): array | |||||
| { | |||||
| return [ | |||||
| 'url' => $this->getVerifyUrl(), | |||||
| ]; | |||||
| } | |||||
| private function getVerifyUrl(): string | |||||
| { | |||||
| return $this->getAppUrl(['change', 'email', 'verify', $this->token->token]); | |||||
| } | |||||
| } | |||||
| @@ -8,7 +8,7 @@ abstract class Members extends BaseEmailer | |||||
| { | { | ||||
| public function getParams(): array | public function getParams(): array | ||||
| { | { | ||||
| return array_merge($this->getGuestParams(), []); | |||||
| return array_merge($this->getMemberParams(), []); | |||||
| } | } | ||||
| abstract public function getMemberParams(): array; | abstract public function getMemberParams(): array; | ||||
| @@ -0,0 +1,182 @@ | |||||
| <?php | |||||
| namespace App\Files; | |||||
| use App\Middlewares\Now; | |||||
| use Exception; | |||||
| use Illuminate\Http\UploadedFile; | |||||
| use Illuminate\Support\Carbon; | |||||
| use Illuminate\Support\Collection; | |||||
| use Illuminate\Support\Facades\Crypt; | |||||
| use Illuminate\Support\Facades\Storage; | |||||
| abstract class BaseFile | |||||
| { | |||||
| protected UploadedFile|null $file = null; | |||||
| private bool $commit = false; | |||||
| protected Carbon|null $updatedAt = null; | |||||
| /** | |||||
| * ディレクトリのパスを取得 | |||||
| * | |||||
| * @return string | |||||
| */ | |||||
| abstract public function getDir(): string; | |||||
| /** | |||||
| * ファイル名の取得 | |||||
| * | |||||
| * @return string | |||||
| */ | |||||
| abstract public function getFilename(): string; | |||||
| /** | |||||
| * MIMETYPEの取得 | |||||
| * | |||||
| * @return string | |||||
| */ | |||||
| abstract public function getMimetype(): string; | |||||
| /** | |||||
| * DBの登録などを定義 | |||||
| * | |||||
| * @return boolean | |||||
| */ | |||||
| abstract protected function onUpload(Carbon $timestamp): bool; | |||||
| /** | |||||
| * コミットする | |||||
| * | |||||
| * @param array<BaseFile>|Collection<BaseFile>|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(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,72 @@ | |||||
| <?php | |||||
| namespace App\Files; | |||||
| use Illuminate\Support\Collection; | |||||
| use Illuminate\Support\Facades\Storage; | |||||
| use Illuminate\Support\Str; | |||||
| use LogicException; | |||||
| class CsvFile extends TmpFile | |||||
| { | |||||
| const ENCODE_UTF8 = "UTF8"; | |||||
| const ENCODE_SJIS = "SJIS"; | |||||
| public function __construct( | |||||
| protected array $headers = [], | |||||
| protected string $encode = self::ENCODE_UTF8 | |||||
| ) { | |||||
| parent::__construct(); | |||||
| if (!in_array($encode, [static::ENCODE_UTF8, static::ENCODE_SJIS])) { | |||||
| throw new LogicException("エンコード指定不正:" . $encode); | |||||
| } | |||||
| if (count($headers) !== 0) { | |||||
| $this->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; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,22 @@ | |||||
| <?php | |||||
| namespace App\Files; | |||||
| class Image | |||||
| { | |||||
| protected string $binary; | |||||
| protected string $mimetype; | |||||
| public function __construct(BaseFile $file) | |||||
| { | |||||
| $this->binary = $file->get(); | |||||
| $this->mimetype = $file->getMimetype(); | |||||
| } | |||||
| public function __toString() | |||||
| { | |||||
| return sprintf("data:%s;base64,%s", $this->mimetype, base64_encode($this->binary)); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,130 @@ | |||||
| <?php | |||||
| namespace App\Files; | |||||
| use App\Jobs\File\DeleteFile; | |||||
| use App\Util\DateUtil; | |||||
| use Illuminate\Contracts\Filesystem\FileNotFoundException; | |||||
| use Illuminate\Support\Carbon; | |||||
| use Illuminate\Support\Facades\Storage; | |||||
| use Illuminate\Support\Str; | |||||
| class TmpFile | |||||
| { | |||||
| final protected const BASE_DIR = "/tmp"; | |||||
| protected const DIR = []; | |||||
| /** | |||||
| * @param string $id | |||||
| * @return static | |||||
| * @throws FileNotFoundException | |||||
| */ | |||||
| static public function loadFile(string $id, ...$other): static | |||||
| { | |||||
| $file = new static($id, $other); | |||||
| if (!$file->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; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,40 @@ | |||||
| <?php | |||||
| namespace App\Http\Controllers\Web\Customer; | |||||
| use App\Http\Controllers\Web\WebController; | |||||
| use App\Logic\EmailChangeManager; | |||||
| use Illuminate\Http\JsonResponse; | |||||
| use Illuminate\Http\Request; | |||||
| use Illuminate\Support\Facades\Auth; | |||||
| class ChangeEmailStartController extends WebController | |||||
| { | |||||
| public function name(): string | |||||
| { | |||||
| return "Email変更開始"; | |||||
| } | |||||
| public function description(): string | |||||
| { | |||||
| return "Emailの変更手続きを開始する"; | |||||
| } | |||||
| public function __construct(protected ChangeEmailStartParams $param, private EmailChangeManager $manager) | |||||
| { | |||||
| parent::__construct(); | |||||
| $this->middleware('auth:sanctum'); | |||||
| } | |||||
| protected function run(Request $request): JsonResponse | |||||
| { | |||||
| $param = $this->param; | |||||
| $this->manager->setUser(Auth::user()) | |||||
| ->generate($param->newEmail); | |||||
| return $this->successResponse(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,19 @@ | |||||
| <?php | |||||
| namespace App\Http\Controllers\Web\Customer; | |||||
| use App\Http\Controllers\Web\BaseParam; | |||||
| use App\Http\Controllers\Web\Rule; | |||||
| /** | |||||
| * @property string $newEmail | |||||
| */ | |||||
| class ChangeEmailStartParams extends BaseParam | |||||
| { | |||||
| public function rules(): array | |||||
| { | |||||
| return [ | |||||
| 'new_email' => $this->str([...Rule::email()]), | |||||
| ]; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,44 @@ | |||||
| <?php | |||||
| namespace App\Http\Controllers\Web\Customer; | |||||
| use App\Http\Controllers\Web\WebController; | |||||
| use App\Logic\EmailChangeManager; | |||||
| use Illuminate\Http\JsonResponse; | |||||
| use Illuminate\Http\Request; | |||||
| use Illuminate\Support\Facades\Auth; | |||||
| class ChangeEmailVerifyController extends WebController | |||||
| { | |||||
| public function name(): string | |||||
| { | |||||
| return "Email変更認証"; | |||||
| } | |||||
| public function description(): string | |||||
| { | |||||
| return "Emailの変更手続きを認証する"; | |||||
| } | |||||
| public function __construct(protected ChangeEmailVerifyParams $param, private EmailChangeManager $manager) | |||||
| { | |||||
| parent::__construct(); | |||||
| // $this->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(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,19 @@ | |||||
| <?php | |||||
| namespace App\Http\Controllers\Web\Customer; | |||||
| use App\Http\Controllers\Web\BaseParam; | |||||
| use App\Http\Controllers\Web\Rule; | |||||
| /** | |||||
| * @property string $token | |||||
| */ | |||||
| class ChangeEmailVerifyParams extends BaseParam | |||||
| { | |||||
| public function rules(): array | |||||
| { | |||||
| return [ | |||||
| 'token' => $this->str(), | |||||
| ]; | |||||
| } | |||||
| } | |||||
| @@ -2,9 +2,11 @@ | |||||
| namespace App\Kintone; | namespace App\Kintone; | ||||
| use App\Exceptions\AppCommonException; | |||||
| use App\Exceptions\ConfigException; | use App\Exceptions\ConfigException; | ||||
| use App\Kintone\Models\KintoneModel; | use App\Kintone\Models\KintoneModel; | ||||
| use Exception; | use Exception; | ||||
| use Illuminate\Database\Eloquent\ModelNotFoundException; | |||||
| use Illuminate\Http\UploadedFile; | use Illuminate\Http\UploadedFile; | ||||
| use Illuminate\Support\Collection; | use Illuminate\Support\Collection; | ||||
| use Illuminate\Support\Facades\Http; | use Illuminate\Support\Facades\Http; | ||||
| @@ -165,6 +167,21 @@ class KintoneAccess | |||||
| return $ret; | 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 | * @param TValue $model | ||||
| * @return void | * @return void | ||||
| @@ -0,0 +1,109 @@ | |||||
| <?php | |||||
| namespace App\Logic; | |||||
| use App\Email\Guests\ChangeEmailStart; | |||||
| use App\Exceptions\GeneralErrorMessageException; | |||||
| use App\Features\InstanceAble; | |||||
| use App\Kintone\Models\Customer; | |||||
| use App\Models\EmailChangeToken; | |||||
| use App\Models\Feature\UserId; | |||||
| use App\Models\User; | |||||
| use App\Util\DateUtil; | |||||
| use Illuminate\Support\Str; | |||||
| use LogicException; | |||||
| class EmailChangeManager | |||||
| { | |||||
| use InstanceAble, UserId; | |||||
| private ?User $user = null; | |||||
| private ?EmailChangeToken $model = null; | |||||
| public function setUser(User $user): static | |||||
| { | |||||
| $this->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); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,126 @@ | |||||
| <?php | |||||
| namespace App\Logic; | |||||
| use App\Events\Email\ConfirmEvent; | |||||
| use App\Exceptions\AppCommonException; | |||||
| use App\Email\BaseEmailer; | |||||
| use App\Models\Email; | |||||
| use App\Models\EmailAttachment; | |||||
| use Exception; | |||||
| use Illuminate\Database\Eloquent\Collection; | |||||
| use Illuminate\Support\Carbon; | |||||
| use Validator; | |||||
| class EmailManager | |||||
| { | |||||
| private Email $model; | |||||
| private bool $canSend = false; | |||||
| /** | |||||
| * @var Collection<int, EmailAttachment>|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; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,24 @@ | |||||
| <?php | |||||
| namespace App\Models; | |||||
| use Illuminate\Database\Eloquent\Relations\HasMany; | |||||
| class Email extends AppModel | |||||
| { | |||||
| const COL_NAME_SEND_DATETIME = "send_datetime"; | |||||
| public function getModelName(): string | |||||
| { | |||||
| return "Email"; | |||||
| } | |||||
| public function emailAttachments(): HasMany | |||||
| { | |||||
| return $this->hasMany(EmailAttachment::class); | |||||
| } | |||||
| protected $casts = [ | |||||
| self::COL_NAME_SEND_DATETIME => 'datetime', | |||||
| ]; | |||||
| } | |||||
| @@ -0,0 +1,17 @@ | |||||
| <?php | |||||
| namespace App\Models; | |||||
| class EmailAttachment extends AppModel | |||||
| { | |||||
| public function getModelName(): string | |||||
| { | |||||
| return "Email添付ファイル"; | |||||
| } | |||||
| public function getHistory(): ?HistoryModel | |||||
| { | |||||
| return null; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,37 @@ | |||||
| <?php | |||||
| namespace App\Models; | |||||
| use App\Models\Feature\UserId; | |||||
| use App\Util\DateUtil; | |||||
| use Illuminate\Database\Eloquent\Builder; | |||||
| class EmailChangeToken extends AppModel | |||||
| { | |||||
| use UserId; | |||||
| const COL_NAME_USER_ID = ColumnName::USER_ID; | |||||
| const COL_NAME_NEW_EMAIL = 'new_email'; | |||||
| const COL_NAME_TOKEN = 'token'; | |||||
| const COL_NAME_EXPIRES_AT = 'expires_at'; | |||||
| protected $casts = [ | |||||
| self::COL_NAME_EXPIRES_AT => '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()); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,12 @@ | |||||
| <?php | |||||
| namespace App\Models; | |||||
| class EmailHistory extends HistoryModel | |||||
| { | |||||
| public function getModelName(): string | |||||
| { | |||||
| return "Email履歴"; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,13 @@ | |||||
| <?php | |||||
| namespace App\Models\Feature; | |||||
| use App\Models\User; | |||||
| trait UserId | |||||
| { | |||||
| public function user() | |||||
| { | |||||
| return $this->belongsTo(User::class); | |||||
| } | |||||
| } | |||||
| @@ -43,6 +43,8 @@ return [ | |||||
| 'password' => env('MAIL_PASSWORD'), | 'password' => env('MAIL_PASSWORD'), | ||||
| 'timeout' => null, | 'timeout' => null, | ||||
| 'local_domain' => env('MAIL_EHLO_DOMAIN'), | 'local_domain' => env('MAIL_EHLO_DOMAIN'), | ||||
| 'auth_mode' => null, | |||||
| 'verify_peer' => false, | |||||
| ], | ], | ||||
| 'ses' => [ | 'ses' => [ | ||||
| @@ -0,0 +1,32 @@ | |||||
| <?php | |||||
| use Illuminate\Database\Migrations\Migration; | |||||
| use Illuminate\Database\Schema\Blueprint; | |||||
| use Illuminate\Support\Facades\Schema; | |||||
| return new class extends Migration | |||||
| { | |||||
| /** | |||||
| * Run the migrations. | |||||
| */ | |||||
| public function up(): void | |||||
| { | |||||
| Schema::create('jobs', function (Blueprint $table) { | |||||
| $table->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'); | |||||
| } | |||||
| }; | |||||
| @@ -0,0 +1,39 @@ | |||||
| <?php | |||||
| use App\Models\ColumnName; | |||||
| use App\Util\MigrationHelper; | |||||
| use Illuminate\Database\Migrations\Migration; | |||||
| use Illuminate\Database\Schema\Blueprint; | |||||
| use Illuminate\Support\Facades\Schema; | |||||
| return new class extends Migration | |||||
| { | |||||
| /** | |||||
| * Run the migrations. | |||||
| */ | |||||
| public function up(): void | |||||
| { | |||||
| Schema::create('email_change_tokens', function (Blueprint $table) { | |||||
| $helper = new MigrationHelper($table); | |||||
| $helper->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'); | |||||
| } | |||||
| }; | |||||
| @@ -0,0 +1,54 @@ | |||||
| <?php | |||||
| use App\Models\ColumnName; | |||||
| use App\Util\MigrationHelper; | |||||
| use Illuminate\Database\Migrations\Migration; | |||||
| use Illuminate\Database\Schema\Blueprint; | |||||
| use Illuminate\Support\Facades\Schema; | |||||
| return new class extends Migration | |||||
| { | |||||
| /** | |||||
| * Run the migrations. | |||||
| * | |||||
| * @return void | |||||
| */ | |||||
| public function up() | |||||
| { | |||||
| MigrationHelper::createTable('emails', $this->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']); | |||||
| }; | |||||
| } | |||||
| }; | |||||
| @@ -0,0 +1,46 @@ | |||||
| <?php | |||||
| use App\Models\ColumnName; | |||||
| use App\Util\MigrationHelper; | |||||
| use Illuminate\Database\Migrations\Migration; | |||||
| use Illuminate\Database\Schema\Blueprint; | |||||
| use Illuminate\Support\Facades\Schema; | |||||
| return new class extends Migration | |||||
| { | |||||
| /** | |||||
| * Run the migrations. | |||||
| * | |||||
| * @return void | |||||
| */ | |||||
| public function up() | |||||
| { | |||||
| MigrationHelper::createTable('email_attachments', $this->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]); | |||||
| }; | |||||
| } | |||||
| }; | |||||
| @@ -2,7 +2,7 @@ version: '3' | |||||
| services: | services: | ||||
| laravel.test: | laravel.test: | ||||
| build: | build: | ||||
| context: ./vendor/laravel/sail/runtimes/8.2 | |||||
| context: ./docker/8.2 | |||||
| dockerfile: Dockerfile | dockerfile: Dockerfile | ||||
| args: | args: | ||||
| WWWGROUP: '${WWWGROUP}' | WWWGROUP: '${WWWGROUP}' | ||||
| @@ -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"] | |||||
| @@ -0,0 +1,4 @@ | |||||
| [PHP] | |||||
| post_max_size = 100M | |||||
| upload_max_filesize = 100M | |||||
| variables_order = EGPCS | |||||
| @@ -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 | |||||
| @@ -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 | |||||
| @@ -0,0 +1 @@ | |||||
| {{ $contents }} | |||||
| @@ -0,0 +1,6 @@ | |||||
| @extends('emails.layouts.guest') | |||||
| @section('contents') | |||||
| こちらのURLにアクセスし、メールアドレスの変更手続きを進めてください。 | |||||
| {{ $url }} | |||||
| @endsection | |||||
| @@ -0,0 +1,11 @@ | |||||
| ご利用ありがとうございます。 | |||||
| MyPageです。 | |||||
| @yield('contents') | |||||
| ※このメールをお送りしているアドレスは、送信専用となっており、返信いただいてもご回答いたしかねます。 | |||||
| ※このメールにお心当たりのない方は、お手数ですが削除いただきますようお願いいたします。 | |||||
| --------------------------------------------------------------------------------------- | |||||
| MyPage | |||||
| @@ -0,0 +1,11 @@ | |||||
| ご利用ありがとうございます。 | |||||
| EasyReceiptです。 | |||||
| @yield('contents') | |||||
| ※このメールをお送りしているアドレスは、送信専用となっており、返信いただいてもご回答いたしかねます。 | |||||
| ※このメールにお心当たりのない方は、お手数ですが削除いただきますようお願いいたします。 | |||||
| --------------------------------------------------------------------------------------- | |||||
| EasyReceipt | |||||
| @@ -0,0 +1 @@ | |||||
| テストメールです | |||||
| @@ -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('/ask', App\Http\Controllers\Web\FAQ\AskController::class); | ||||
| RouteHelper::post('/upload/student-license-images', App\Http\Controllers\Web\Customer\UploadStudentLicenseImagesController::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('/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); | |||||