Browse Source

Email変更手続き 追加

master
sosuke.iwabuchi 2 years ago
parent
commit
72c266a297
36 changed files with 1344 additions and 18 deletions
  1. +67
    -0
      app/Console/Commands/TestEmail.php
  2. +0
    -16
      app/Email/BaseEmailer.php
  3. +35
    -0
      app/Email/Guests/ChangeEmailStart.php
  4. +1
    -1
      app/Email/Members/Members.php
  5. +182
    -0
      app/Files/BaseFile.php
  6. +72
    -0
      app/Files/CsvFile.php
  7. +22
    -0
      app/Files/Image.php
  8. +130
    -0
      app/Files/TmpFile.php
  9. +40
    -0
      app/Http/Controllers/Web/Customer/ChangeEmailStartController.php
  10. +19
    -0
      app/Http/Controllers/Web/Customer/ChangeEmailStartParams.php
  11. +44
    -0
      app/Http/Controllers/Web/Customer/ChangeEmailVerifyController.php
  12. +19
    -0
      app/Http/Controllers/Web/Customer/ChangeEmailVerifyParams.php
  13. +17
    -0
      app/Kintone/KintoneAccess.php
  14. +109
    -0
      app/Logic/EmailChangeManager.php
  15. +126
    -0
      app/Logic/EmailManager.php
  16. +24
    -0
      app/Models/Email.php
  17. +17
    -0
      app/Models/EmailAttachment.php
  18. +37
    -0
      app/Models/EmailChangeToken.php
  19. +12
    -0
      app/Models/EmailHistory.php
  20. +13
    -0
      app/Models/Feature/UserId.php
  21. +2
    -0
      config/mail.php
  22. +32
    -0
      database/migrations/2023_04_13_100116_create_jobs_table.php
  23. +39
    -0
      database/migrations/2023_09_14_131600_create_email_change_tokens_table.php
  24. +54
    -0
      database/migrations/2023_09_14_151800_create_emails_table.php
  25. +46
    -0
      database/migrations/2023_09_14_153300_create_email_attachments_table.php
  26. +1
    -1
      docker-compose.yml
  27. +62
    -0
      docker/8.2/Dockerfile
  28. +4
    -0
      docker/8.2/php.ini
  29. +17
    -0
      docker/8.2/start-container
  30. +68
    -0
      docker/8.2/supervisord.conf
  31. +1
    -0
      resources/views/emails/free_text.blade.php
  32. +6
    -0
      resources/views/emails/guests/change_email_start.blade.php
  33. +11
    -0
      resources/views/emails/layouts/guest.blade.php
  34. +11
    -0
      resources/views/emails/layouts/member.blade.php
  35. +1
    -0
      resources/views/emails/test.blade.php
  36. +3
    -0
      routes/api.php

+ 67
- 0
app/Console/Commands/TestEmail.php View File

@@ -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;
}
}

+ 0
- 16
app/Email/BaseEmailer.php View File

@@ -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;
} }


+ 35
- 0
app/Email/Guests/ChangeEmailStart.php View File

@@ -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]);
}
}

+ 1
- 1
app/Email/Members/Members.php View File

@@ -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;


+ 182
- 0
app/Files/BaseFile.php View File

@@ -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();
}
}

+ 72
- 0
app/Files/CsvFile.php View File

@@ -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;
}
}

+ 22
- 0
app/Files/Image.php View File

@@ -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));
}
}

+ 130
- 0
app/Files/TmpFile.php View File

@@ -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;
}
}
}

+ 40
- 0
app/Http/Controllers/Web/Customer/ChangeEmailStartController.php View File

@@ -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();
}
}

+ 19
- 0
app/Http/Controllers/Web/Customer/ChangeEmailStartParams.php View File

@@ -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()]),
];
}
}

+ 44
- 0
app/Http/Controllers/Web/Customer/ChangeEmailVerifyController.php View File

@@ -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();
}
}

+ 19
- 0
app/Http/Controllers/Web/Customer/ChangeEmailVerifyParams.php View File

@@ -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(),
];
}
}

+ 17
- 0
app/Kintone/KintoneAccess.php View File

@@ -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


+ 109
- 0
app/Logic/EmailChangeManager.php View File

@@ -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);
}
}

+ 126
- 0
app/Logic/EmailManager.php View File

@@ -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;
}
}

+ 24
- 0
app/Models/Email.php View File

@@ -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',
];
}

+ 17
- 0
app/Models/EmailAttachment.php View File

@@ -0,0 +1,17 @@
<?php

namespace App\Models;


class EmailAttachment extends AppModel
{
public function getModelName(): string
{
return "Email添付ファイル";
}

public function getHistory(): ?HistoryModel
{
return null;
}
}

+ 37
- 0
app/Models/EmailChangeToken.php View File

@@ -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());
}
}

+ 12
- 0
app/Models/EmailHistory.php View File

@@ -0,0 +1,12 @@
<?php

namespace App\Models;


class EmailHistory extends HistoryModel
{
public function getModelName(): string
{
return "Email履歴";
}
}

+ 13
- 0
app/Models/Feature/UserId.php View File

@@ -0,0 +1,13 @@
<?php

namespace App\Models\Feature;

use App\Models\User;

trait UserId
{
public function user()
{
return $this->belongsTo(User::class);
}
}

+ 2
- 0
config/mail.php View File

@@ -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' => [


+ 32
- 0
database/migrations/2023_04_13_100116_create_jobs_table.php View File

@@ -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');
}
};

+ 39
- 0
database/migrations/2023_09_14_131600_create_email_change_tokens_table.php View File

@@ -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');
}
};

+ 54
- 0
database/migrations/2023_09_14_151800_create_emails_table.php View File

@@ -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']);
};
}
};

+ 46
- 0
database/migrations/2023_09_14_153300_create_email_attachments_table.php View File

@@ -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]);
};
}
};

+ 1
- 1
docker-compose.yml View File

@@ -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}'


+ 62
- 0
docker/8.2/Dockerfile View File

@@ -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"]

+ 4
- 0
docker/8.2/php.ini View File

@@ -0,0 +1,4 @@
[PHP]
post_max_size = 100M
upload_max_filesize = 100M
variables_order = EGPCS

+ 17
- 0
docker/8.2/start-container View File

@@ -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

+ 68
- 0
docker/8.2/supervisord.conf View File

@@ -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

+ 1
- 0
resources/views/emails/free_text.blade.php View File

@@ -0,0 +1 @@
{{ $contents }}

+ 6
- 0
resources/views/emails/guests/change_email_start.blade.php View File

@@ -0,0 +1,6 @@
@extends('emails.layouts.guest')

@section('contents')
こちらのURLにアクセスし、メールアドレスの変更手続きを進めてください。
{{ $url }}
@endsection

+ 11
- 0
resources/views/emails/layouts/guest.blade.php View File

@@ -0,0 +1,11 @@
ご利用ありがとうございます。
MyPageです。

@yield('contents')


※このメールをお送りしているアドレスは、送信専用となっており、返信いただいてもご回答いたしかねます。
※このメールにお心当たりのない方は、お手数ですが削除いただきますようお願いいたします。

---------------------------------------------------------------------------------------
MyPage

+ 11
- 0
resources/views/emails/layouts/member.blade.php View File

@@ -0,0 +1,11 @@
ご利用ありがとうございます。
EasyReceiptです。

@yield('contents')


※このメールをお送りしているアドレスは、送信専用となっており、返信いただいてもご回答いたしかねます。
※このメールにお心当たりのない方は、お手数ですが削除いただきますようお願いいたします。

---------------------------------------------------------------------------------------
EasyReceipt

+ 1
- 0
resources/views/emails/test.blade.php View File

@@ -0,0 +1 @@
テストメールです

+ 3
- 0
routes/api.php View File

@@ -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);

Loading…
Cancel
Save