| @@ -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 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; | |||
| } | |||
| @@ -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 | |||
| { | |||
| return array_merge($this->getGuestParams(), []); | |||
| return array_merge($this->getMemberParams(), []); | |||
| } | |||
| 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; | |||
| 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 | |||
| @@ -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'), | |||
| 'timeout' => null, | |||
| 'local_domain' => env('MAIL_EHLO_DOMAIN'), | |||
| 'auth_mode' => null, | |||
| 'verify_peer' => false, | |||
| ], | |||
| '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: | |||
| laravel.test: | |||
| build: | |||
| context: ./vendor/laravel/sail/runtimes/8.2 | |||
| context: ./docker/8.2 | |||
| dockerfile: Dockerfile | |||
| args: | |||
| 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('/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); | |||