diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 4e0e80f..ef010a0 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -4,11 +4,9 @@ namespace App\Http\Controllers; use App\Helpers; use App\Jobs\SendEmail; -use App\Mail\EmailUpdateLink; -use App\Mail\RegisterLink; +use App\Mail\UserEmailUpdateRequest; use App\Models\User; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Validator; use Illuminate\Support\Str; use Illuminate\Validation\Rule; @@ -49,7 +47,7 @@ class AccountController extends Controller $validator = Validator::make($request->all(), [ 'firstname' => 'required', 'surname' => 'required', - 'email' => ['required', 'email', Rule::unique('users')->ignore($user->id)], + 'email' => ['required', 'email', 'unique:users,email,' . $user->id], 'phone' => 'required', 'home_address' => 'required_with:home_city,home_postcode,home_country,home_state', @@ -92,20 +90,18 @@ class AccountController extends Controller $newEmail = $userData['email']; unset($userData['email']); - if ($user->email !== $newEmail) { - if(User::where('email', $request->get('email'))->exists()) { - $validator->errors()->add('email', __('validation.custom_messages.email_exists')); - return redirect()->back()->withErrors($validator)->withInput(); - } + if (strtolower($user->email) !== strtolower($newEmail)) { + $user->tokens()->where('type', 'email-update')->delete(); - $token = Str::random(60); - $user->emailUpdate()->delete(); - $emailUpdate = $user->emailUpdate()->create([ - 'email' => $newEmail, - 'token' => $token + $token = $user->tokens()->create([ + 'type' => 'email-update', + 'data' => [ + 'email' => $newEmail, + ], + 'expires_at' => now()->addMinutes(30), ]); - dispatch(new SendEmail($user->email, new EmailUpdateLink($token, $user->getName(), $user->email, $newEmail)))->onQueue('mail'); + dispatch(new SendEmail($user->email, new UserEmailUpdateRequest($token->id, $user->email, $newEmail)))->onQueue('mail'); } $userData['subscribed'] = ($request->get('subscribed', false) === 'on'); diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index d5d09df..c555413 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -3,57 +3,89 @@ namespace App\Http\Controllers; use App\Jobs\SendEmail; -use App\Mail\LoginLink; -use App\Mail\RegisterLink; -use App\Models\EmailSubscriptions; -use App\Models\EmailUpdate; +use App\Mail\UserEmailUpdateConfirm; +use App\Mail\UserLogin; +use App\Mail\UserRegister; +use App\Mail\UserWelcome; +use App\Models\Token; use App\Models\User; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; +use Illuminate\View\View; class AuthController extends Controller { - public function showLogin(Request $request) { + /** + * Show the login form or if token present, process the login + * + * @param Request $request + * @return View|RedirectResponse + */ + public function showLogin(Request $request): View|RedirectResponse + { if (auth()->check()) { -// return redirect()->route('dashboard'); return redirect()->action([HomeController::class, 'index']); } $token = $request->query('token'); if ($token) { - return $this->tokenLogin($token); + return $this->LoginByToken($token); } return view('auth.login'); } - public function tokenLogin($token) + /** + * Process the login form + * + * @param Request $request + * @return View|RedirectResponse + */ + public function postLogin(Request $request): View|RedirectResponse { - $loginToken = DB::table('login_tokens')->where('token', $token)->first(); + $request->validate([ + 'email' => 'required|email', + ], [ + 'email.required' => __('validation.custom_messages.email_required'), + 'email.email' => __('validation.custom_messages.email_invalid'), + ]); - if ($loginToken) { - $user = User::where('email', $loginToken->email)->first(); - $intended_url = $loginToken->intended_url; + $user = User::where('email', $request->email)->whereNotNull('email_verified_at')->first(); + if($user) { + $token = $user->tokens()->create([ + 'type' => 'login', + 'data' => ['url' => session()->pull('url.intended', null)], + ]); - DB::table('login_tokens')->where('token', $token)->delete(); + dispatch(new SendEmail($user->email, new UserLogin($token->id, $user->getName(), $user->email)))->onQueue('mail'); + return view('auth.login-link'); + } - if ($user) { - Auth::login($user); + session()->flash('status', 'not-found'); + return view('auth.login'); + } - $user->markEmailAsVerified(); - DB::table('login_tokens')->where('token', $token)->delete(); - session()->flash('message', 'You have been logged in'); - session()->flash('message-title', 'Logged in'); - session()->flash('message-type', 'success'); + /** + * Process the login by token + * + * @param string $tokenStr + * @return View|RedirectResponse + */ + public function loginByToken(string $tokenStr): View|RedirectResponse + { + $token = Token::where('id', $tokenStr) + ->where('type', 'login') + ->where('expires_at', '>', now()) + ->first(); - if($intended_url) { - return redirect($intended_url); - } - - return redirect()->action([HomeController::class, 'index']); + if ($token) { + $user = $token->user; + if($user) { + $token->delete(); + return $this->loginByUser($user, $token->data); } } @@ -63,27 +95,40 @@ class AuthController extends Controller return view('auth.login'); } - public function postLogin(Request $request) { - $request->validate([ - 'email' => 'required|email', - ], [ - 'email.required' => __('validation.custom_messages.email_required'), - 'email.email' => __('validation.custom_messages.email_invalid'), - ]); - - $user = User::where('email', $request->email)->first(); - if($user) { - $token = $user->createLoginToken(session()->pull('url.intended', null)); - dispatch(new SendEmail($user->email, new LoginLink($token, $user->getName(), $user->email)))->onQueue('mail'); - - return view('auth.login-link'); + /** + * Process the login by user + * + * @param User $user + * @param array $data + * @return RedirectResponse + */ + public function loginByUser(User $user, array $data = []) + { + $url = null; + if($data && isset($data->url) && $data->url) { + $url = $data->url; } - session()->flash('status', 'not-found'); - return view('auth.login'); + Auth::login($user); + + session()->flash('message', 'You have been logged in'); + session()->flash('message-title', 'Logged in'); + session()->flash('message-type', 'success'); + + if($url) { + return redirect($url); + } + + return redirect()->action([HomeController::class, 'index']); } - public function logout() { + /** + * Process the user logout + * + * @return RedirectResponse + */ + public function logout(): RedirectResponse + { auth()->logout(); session()->flash('message', 'You have been logged out'); @@ -92,15 +137,57 @@ class AuthController extends Controller return redirect()->route('index'); } - public function showRegister(Request $request) { + /** + * Show the registration form or if token present, process the registration + * + * @param Request $request + * @return View|RedirectResponse + */ + public function showRegister(Request $request): View|RedirectResponse + { if (auth()->check()) { return redirect()->route('index'); } + $tokenStr = $request->query('token'); + if ($tokenStr) { + $token = Token::where('id', $tokenStr) + ->where('type', 'register') + ->where('expires_at', '>', now()) + ->first(); + + if ($token) { + $user = $token->user; + if ($user) { + $user->email_verified_at = now(); + $user->save(); + + $user->tokens()->where('type', 'register')->delete(); + + dispatch(new SendEmail($user->email, new UserWelcome($user->email)))->onQueue('mail'); + + $this->loginByUser($user); + return redirect()->route('index'); + } + } + + session()->flash('message', 'That token has expired or is invalid'); + session()->flash('message-title', 'Registration failed'); + session()->flash('message-type', 'danger'); + } + + return view('auth.register'); } - public function postRegister(Request $request) { + /** + * Process the registration form + * + * @param Request $request + * @return View|RedirectResponse + */ + public function postRegister(Request $request): View|RedirectResponse + { $request->validate([ 'email' => 'required|email', ], [ @@ -119,46 +206,65 @@ class AuthController extends Controller ]); } } else if($passHoneypot) { - $firstname = explode('@', $request->email)[0]; - $user = User::create([ - 'firstname' => $firstname, 'email' => $request->email, ]); - - EmailUpdate::where('email', $request->email)->delete(); } if($passHoneypot) { Log::channel('honeypot')->info('Valid key used for registration using email: ' . $request->email . ', ip address: ' . $request->ip() . ', user agent: ' . $request->userAgent()); - $token = $user->createLoginToken(session()->pull('url.intended', null)); - dispatch(new SendEmail($user->email, new RegisterLink($token, $user->getName(), $user->email)))->onQueue('mail'); + $user->tokens()->where('type', 'register')->delete(); + $token = $user->tokens()->create([ + 'type' => 'register', + 'data' => ['url' => session()->pull('url.intended', null)], + ]); + + dispatch(new SendEmail($user->email, new UserRegister($token->id, $user->email)))->onQueue('mail'); } else { Log::channel('honeypot')->info('Invalid key used for registration using email: ' . $request->email . ', ip address: ' . $request->ip() . ', user agent: ' . $request->userAgent() . ', key: ' . $key); } - return view('auth.login-link'); + return view('auth.register-link'); } - public function updateEmail(Request $request) + /** + * Confirm the user email update. + * + * @param Request $request + * @return RedirectResponse + */ + public function updateEmail(Request $request): RedirectResponse { - $token = $request->query('token'); - $emailUpdate = EmailUpdate::where('token', $token)->first(); - if($emailUpdate && $emailUpdate->user) { - $emailUpdate->user->email = $emailUpdate->email; - $emailUpdate->user->email_verified_at = now(); - $emailUpdate->user->save(); - $emailUpdate->delete(); + $tokenStr = $request->query('token'); - session()->flash('message', 'Your email has been updated'); - session()->flash('message-title', 'Email updated'); - session()->flash('message-type', 'success'); - return redirect()->route('index'); + $token = Token::where('id', $tokenStr) + ->where('type', 'email-update') + ->where('expires_at', '>', now()) + ->first(); + + if($token && $token->user) { + if($token->data && isset($token->data['email'])) { + $user = $token->user; + $user->email = $token->data['email']; + $user->email_verified_at = now(); + $user->save(); + + $user->tokens()->where('type', 'email-update')->delete(); + + session()->flash('message', 'Your email has been updated'); + session()->flash('message-title', 'Email updated'); + session()->flash('message-type', 'success'); + + dispatch(new SendEmail($user->email, new UserEmailUpdateConfirm($user->email)))->onQueue('mail'); + + return redirect()->route('index'); + } } session()->flash('message', 'That token has expired or is invalid'); session()->flash('message-title', 'Email update failed'); session()->flash('message-type', 'danger'); + return redirect()->route('index'); } } diff --git a/app/Mail/UserEmailUpdateConfirm.php b/app/Mail/UserEmailUpdateConfirm.php new file mode 100644 index 0000000..429e7c6 --- /dev/null +++ b/app/Mail/UserEmailUpdateConfirm.php @@ -0,0 +1,30 @@ +email = $email; + } + + public function build() + { + return $this + ->subject('Your STEMMechanics account has been updated 👍') + ->markdown('emails.email-update-confirm') + ->with([ + 'email' => $this->email, + ]); + } +} diff --git a/app/Mail/EmailUpdateLink.php b/app/Mail/UserEmailUpdateRequest.php similarity index 61% rename from app/Mail/EmailUpdateLink.php rename to app/Mail/UserEmailUpdateRequest.php index 6f41744..ecb66c1 100644 --- a/app/Mail/EmailUpdateLink.php +++ b/app/Mail/UserEmailUpdateRequest.php @@ -7,19 +7,17 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; -class EmailUpdateLink extends Mailable +class UserEmailUpdateRequest extends Mailable { use Queueable, SerializesModels; public $token; - public $username; public $email; public $newEmail; - public function __construct($token, $username, $email, $newEmail) + public function __construct($token, $email, $newEmail) { $this->token = $token; - $this->username = $username; $this->email = $email; $this->newEmail = $newEmail; } @@ -27,11 +25,10 @@ class EmailUpdateLink extends Mailable public function build() { return $this - ->subject('Confirm new email address') - ->markdown('emails.change-email-link') + ->subject('Almost There! Confirm Your New Email Address 👍') + ->markdown('emails.email-update-request') ->with([ - 'token' => $this->token, - 'username' => $this->username, + 'update_url' => route('update.email', ['token' => $this->token]), 'email' => $this->email, 'newEmail' => $this->newEmail, ]); diff --git a/app/Mail/LoginLink.php b/app/Mail/UserLogin.php similarity index 75% rename from app/Mail/LoginLink.php rename to app/Mail/UserLogin.php index 38517c8..e8bf2b1 100644 --- a/app/Mail/LoginLink.php +++ b/app/Mail/UserLogin.php @@ -7,7 +7,7 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; -class LoginLink extends Mailable +class UserLogin extends Mailable { use Queueable, SerializesModels; @@ -25,10 +25,10 @@ class LoginLink extends Mailable public function build() { return $this - ->subject('Here\'s your login link') - ->markdown('emails.login-link') + ->subject('Here\'s your login link 🤫') + ->markdown('emails.login') ->with([ - 'token' => $this->token, + 'login_url' => route('login', ['token' => $this->token]), 'username' => $this->username, 'email' => $this->email, ]); diff --git a/app/Mail/RegisterLink.php b/app/Mail/UserRegister.php similarity index 57% rename from app/Mail/RegisterLink.php rename to app/Mail/UserRegister.php index 646f4b5..9b72edc 100644 --- a/app/Mail/RegisterLink.php +++ b/app/Mail/UserRegister.php @@ -7,29 +7,26 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; -class RegisterLink extends Mailable +class UserRegister extends Mailable { use Queueable, SerializesModels; public $token; - public $username; public $email; - public function __construct($token, $username, $email) + public function __construct($token, $email) { $this->token = $token; - $this->username = $username; $this->email = $email; } public function build() { return $this - ->subject('Here\'s your registration link') - ->markdown('emails.register-link') + ->subject('Almost There! Just One More Step to Join Us 🚀') + ->markdown('emails.register') ->with([ - 'token' => $this->token, - 'username' => $this->username, + 'register_url' => route('register', ['token' => $this->token]), 'email' => $this->email, ]); } diff --git a/app/Mail/UserWelcome.php b/app/Mail/UserWelcome.php new file mode 100644 index 0000000..0bd413d --- /dev/null +++ b/app/Mail/UserWelcome.php @@ -0,0 +1,30 @@ +email = $email; + } + + public function build() + { + return $this + ->subject('Welcome to STEMMechanics 🌟') + ->markdown('emails.welcome') + ->with([ + 'email' => $this->email, + ]); + } +} diff --git a/app/Models/EmailUpdate.php b/app/Models/EmailUpdate.php deleted file mode 100644 index 3f2501f..0000000 --- a/app/Models/EmailUpdate.php +++ /dev/null @@ -1,30 +0,0 @@ -belongsTo(User::class); - } -} diff --git a/app/Models/Token.php b/app/Models/Token.php new file mode 100644 index 0000000..85f3740 --- /dev/null +++ b/app/Models/Token.php @@ -0,0 +1,87 @@ + + */ + protected $fillable = [ + 'user_id', + 'type', + 'data', + 'expires_at', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'expires_at' => 'datetime', + 'data' => 'array', + ]; + + /** + * Indicates if the model should be timestamped. + * + * @var bool + */ + public $timestamps = false; + + /** + * The primary key for the model is incrementing. + * + * @var bool $incrementing + */ + public $incrementing = false; + + /** + * The primary key type for the model. + * + * @var string + */ + public $keyType = 'string'; + + /** + * The "booted" method of the model. + * + * @return void + */ + public static function boot() + { + parent::boot(); + + static::creating(function ($model) { + if (empty($model->{$model->getKeyName()}) === true) { + do { + $newToken = Str::random(48); + } while (self::where($model->getKeyName(), $newToken)->exists()); + + $model->{$model->getKeyName()} = $newToken; + } + + if (empty($model->expires_at) === true) { + $model->expires_at = now()->addMinutes(10); + } + }); + } + + /** + * Get the user that the token belongs to. + * + * @return BelongsTo + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 66430bb..be0781b 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,17 +2,12 @@ namespace App\Models; -use App\Mail\LoginLink; use App\Traits\UUID; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Log; -use Illuminate\Support\Facades\Mail; -use Illuminate\Support\Str; -use PharIo\Manifest\Email; class User extends Authenticatable implements MustVerifyEmail { @@ -110,34 +105,21 @@ class User extends Authenticatable implements MustVerifyEmail }); } - public function createLoginToken($redirect = null) + /** + * Get the tokens for the user. + * + * @return HasMany + */ + public function tokens(): HasMany { - // Generate a unique token - $token = Str::random(60); - - // Store the token in the database - DB::table('login_tokens')->insert([ - 'email' => $this->email, - 'token' => $token, - 'intended_url' => $redirect, - ]); - - return $token; - } - - public function softDelete() - { - foreach ($this->fillable as $field) { - if ($field === 'email_verified_at') { - $this->email_verified_at = null; - } else if ($field !== 'email') { - $this->{$field} = ''; - } - } - - $this->save(); + return $this->hasMany(Token::class); } + /** + * Get the calculated name of the user. + * + * @return string + */ public function getName(): string { $name = ''; @@ -183,14 +165,11 @@ class User extends Authenticatable implements MustVerifyEmail } } - public function emailUpdate() - { - return $this->hasOne(EmailUpdate::class); - } public function getEmailUpdatePendingAttribute() { - return $this->emailUpdate()->exists(); + $emailUpdate = $this->tokens()->where('type', 'email-update')->where('expires_at', '>', now())->first(); + return $emailUpdate ? $emailUpdate->data['email'] : null; } public function isAdmin(): bool diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php index 577d658..56d67dd 100644 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -69,6 +69,7 @@ return new class extends Migration public function down(): void { Schema::dropIfExists('users'); + Schema::dropIfExists('login_tokens'); Schema::dropIfExists('password_reset_tokens'); Schema::dropIfExists('sessions'); } diff --git a/database/migrations/2024_05_06_155300_merge_tokens_table.php b/database/migrations/2024_05_06_155300_merge_tokens_table.php new file mode 100644 index 0000000..069e2ca --- /dev/null +++ b/database/migrations/2024_05_06_155300_merge_tokens_table.php @@ -0,0 +1,56 @@ +string('id')->primary(); + $table->foreignUuid('user_id')->constrained()->onDelete('cascade'); + $table->string('type'); + $table->json('data')->nullable(); + $table->timestamp('expires_at')->nullable(); + $table->timestamp('created_at')->default(DB::raw('CURRENT_TIMESTAMP')); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::create('email_updates', function (Blueprint $table) { + $table->id(); + $table->foreignUuid('user_id')->constrained()->onDelete('cascade'); + $table->string('email'); + $table->string('token')->unique(); + $table->timestamps(); + }); + + Schema::create('password_reset_tokens', function (Blueprint $table) { + $table->string('email')->primary(); + $table->string('token'); + $table->timestamp('created_at')->default(DB::raw('CURRENT_TIMESTAMP')); + }); + + Schema::create('login_tokens', function (Blueprint $table) { + $table->id(); + $table->string('email'); + $table->string('token')->unique(); + $table->string('intended_url')->nullable(); + $table->timestamp('created_at')->default(DB::raw('CURRENT_TIMESTAMP')); + }); + } +}; diff --git a/resources/views/account.blade.php b/resources/views/account.blade.php index 4b6c675..caeef42 100644 --- a/resources/views/account.blade.php +++ b/resources/views/account.blade.php @@ -25,7 +25,7 @@ $billing_same_home = $user->home_address === $user->billing_address
- +
diff --git a/resources/views/auth/register-link.blade.php b/resources/views/auth/register-link.blade.php new file mode 100644 index 0000000..bfd1c91 --- /dev/null +++ b/resources/views/auth/register-link.blade.php @@ -0,0 +1,7 @@ + + + Check your inbox +

Check your email for the registration link we just sent. Click the link to finish setting up your account.

+ Home +
+
diff --git a/resources/views/emails/change-email-link.blade.php b/resources/views/emails/change-email-link.blade.php deleted file mode 100644 index 50c9c3d..0000000 --- a/resources/views/emails/change-email-link.blade.php +++ /dev/null @@ -1,14 +0,0 @@ -@component('mail::message', ['username' => $username, 'email' => $email]) -

Confirm your new email address

-

A request was made to change your email address at STEMMechanics to {{ $newEmail }}.

-

For your security, this link can only be used once and expires after 10 minutes.

-

- @component('mail::button', ['url' => route('update.email', ['token' => $token])]) - Update Email - @endcomponent -

-
-

Why did I get this link?

-

Someone asked to change the email address associated with an account at STEMMechanics with this email.

-

If this wasn't you, you can ignore this email.

-@endcomponent diff --git a/resources/views/emails/email-update-confirm.blade.php b/resources/views/emails/email-update-confirm.blade.php new file mode 100644 index 0000000..a775a96 --- /dev/null +++ b/resources/views/emails/email-update-confirm.blade.php @@ -0,0 +1,6 @@ +@component('mail::message', ['email' => $email]) +

Hey there!

+

Just a quick line to confirm that your email address has now been updated at STEMMechanics!

+

Warm regards,

+

—James 😁

+@endcomponent diff --git a/resources/views/emails/email-update-request.blade.php b/resources/views/emails/email-update-request.blade.php new file mode 100644 index 0000000..2f910df --- /dev/null +++ b/resources/views/emails/email-update-request.blade.php @@ -0,0 +1,16 @@ +@component('mail::message', ['email' => $email]) +

Hey there!

+

You requested to update your email address at STEMMechanics to {{ $newEmail }}. Click the link below to confirm this change:

+

+ @component('mail::button', ['url' => $update_url]) + Update Email + @endcomponent +

+

Remember, the link expires in 30 minutes.

+

Warm regards,

+

—James 😁

+ @slot('subcopy') +

Why did I get this email?

+

Someone asked to change the email address associated with an account at STEMMechanics with this email. If this wasn't you, you can ignore this email.

+ @endslot +@endcomponent diff --git a/resources/views/emails/login-link.blade.php b/resources/views/emails/login-link.blade.php deleted file mode 100644 index 808d88e..0000000 --- a/resources/views/emails/login-link.blade.php +++ /dev/null @@ -1,13 +0,0 @@ -@component('mail::message', ['username' => $username, 'email' => $email]) -

Follow this link to log in to your account.

-

For your security, this link can only be used once and expires after 10 minutes.

-

- @component('mail::button', ['url' => route('login', ['token' => $token])]) - Log in - @endcomponent -

-
-

Why did I get this link?

-

Someone asked for a login link to log in to STEMMechanics with this email.

-

If this wasn't you, you can ignore this email.

-@endcomponent diff --git a/resources/views/emails/login.blade.php b/resources/views/emails/login.blade.php new file mode 100644 index 0000000..be1e1f0 --- /dev/null +++ b/resources/views/emails/login.blade.php @@ -0,0 +1,16 @@ +@component('mail::message', ['email' => $email]) +

Hey there!

+

You requested a link to log in to STEMMechanics, and here it is!

+

+ @component('mail::button', ['url' => $login_url]) + Log in + @endcomponent +

+

Remember, the link expires in 10 minutes and can only be used once.

+

Warm regards,

+

—James 😁

+ @slot('subcopy') +

Why did I get this email?

+

Someone asked for a link to log in to STEMMechanics with this email address. If this wasn't you, you can ignore this email.

+ @endslot +@endcomponent diff --git a/resources/views/emails/register-link.blade.php b/resources/views/emails/register-link.blade.php deleted file mode 100644 index 02a1748..0000000 --- a/resources/views/emails/register-link.blade.php +++ /dev/null @@ -1,13 +0,0 @@ -@component('mail::message', ['username' => $username, 'email' => $email]) -

Follow this link to register your account.

-

For your security, this link can only be used once and expires after 10 minutes.

-

- @component('mail::button', ['url' => route('login', ['token' => $token])]) - Register - @endcomponent -

-
-

Why did I get this link?

-

Someone asked to register an account at STEMMechanics with this email.

-

If this wasn't you, you can ignore this email.

-@endcomponent diff --git a/resources/views/emails/register.blade.php b/resources/views/emails/register.blade.php new file mode 100644 index 0000000..de5dbb9 --- /dev/null +++ b/resources/views/emails/register.blade.php @@ -0,0 +1,17 @@ +@component('mail::message', ['email' => $email]) +

Hey there!

+

We're thrilled to have you join us. To complete your registration and officially become part of the community, just click link below:

+

+@component('mail::button', ['url' => $register_url]) +Register +@endcomponent +

+

Remember, the link expires in 10 minutes and can only be used once, so act fast!

+

Warm regards,

+

—James 😁

+ +@slot('subcopy') +

Why did I get this email?

+

Someone asked to register at STEMMechanics with this email address. If this wasn't you, you can ignore this email.

+@endslot +@endcomponent diff --git a/resources/views/emails/subscribe.blade.php b/resources/views/emails/subscribe.blade.php new file mode 100644 index 0000000..183305e --- /dev/null +++ b/resources/views/emails/subscribe.blade.php @@ -0,0 +1,7 @@ +@component('mail::message', ['email' => $email, 'unsubscribe' => $unsubscribe]) + @include('emails.welcome') +
+

Why did I get this email?

+

Someone asked to subscribe to our mailing list at STEMMechanics with this email address.

+

If this wasn't you, you can unsubscribe.

+@endcomponent diff --git a/resources/views/emails/welcome.blade.php b/resources/views/emails/welcome.blade.php new file mode 100644 index 0000000..0e6096a --- /dev/null +++ b/resources/views/emails/welcome.blade.php @@ -0,0 +1,12 @@ +@component('mail::message', ['email' => $email]) +

Welcome to the community!

+

Really glad to have you here and can't wait to see you at one of our workshops.

+

You'll get information about upcoming workshops as it comes out.

+

Even though this is (of course) an automated email, just wanted to say thanks for registering and intro myself.

+

If you didn't know, I'm James and I'm the founder of STEMMechanics. I promise not to spam you, sell your data, or send you anything I don't think is absolutely necessary.

+

You know a bit about me but I don't know really anything about you...

+

If you're up for it, reply to this email and tell me a bit about yourself and also let me know what workshops you are interested in?

+

I read and reply to every one 😁

+

Talk soon

+

—James

+@endcomponent diff --git a/resources/views/vendor/mail/html/footer.blade.php b/resources/views/vendor/mail/html/footer.blade.php index 3ff41f8..773cecc 100644 --- a/resources/views/vendor/mail/html/footer.blade.php +++ b/resources/views/vendor/mail/html/footer.blade.php @@ -1,11 +1 @@ - - - - - - - - - diff --git a/resources/views/vendor/mail/html/header.blade.php b/resources/views/vendor/mail/html/header.blade.php index 4e26259..1226caa 100644 --- a/resources/views/vendor/mail/html/header.blade.php +++ b/resources/views/vendor/mail/html/header.blade.php @@ -1,7 +1,5 @@ -@props(['url', 'username']) - - - +@props(['url']) + STEMMechanics Logo -

Hello, {{ $username }}

- - diff --git a/resources/views/vendor/mail/html/layout.blade.php b/resources/views/vendor/mail/html/layout.blade.php index e55f6a6..e6c0388 100644 --- a/resources/views/vendor/mail/html/layout.blade.php +++ b/resources/views/vendor/mail/html/layout.blade.php @@ -25,33 +25,46 @@ width: 100% !important; + + + + + + @isset($subcopy) + + + + @endisset + + + + + + + + + + + + + diff --git a/resources/views/vendor/mail/html/message.blade.php b/resources/views/vendor/mail/html/message.blade.php index 57a7628..724f06e 100644 --- a/resources/views/vendor/mail/html/message.blade.php +++ b/resources/views/vendor/mail/html/message.blade.php @@ -1,7 +1,7 @@ {{-- Header --}} - + {{-- Body --}} @@ -10,9 +10,7 @@ {{-- Subcopy --}} @isset($subcopy) - -{{ $subcopy }} - +{{ Illuminate\Mail\Markdown::parse($subcopy) }} @endisset @@ -20,9 +18,9 @@

This email was sent to {{ $email }}
-{{ config('app.name') }} | 1/4 Jordan Street | Edmonton, QLD 4869 Australia
+{{ config('app.name') }} | 1/4 Jordan Street | Edmonton, QLD 4869 Australia
© {{ date('Y') }} {{ config('app.name') }}. {{ __('All rights reserved.') }}
-Privacy Policy +Privacy Policy | Terms & Conditions @isset($unsubscribe) | Unsubscribe@endisset

diff --git a/resources/views/vendor/mail/html/subcopy.blade.php b/resources/views/vendor/mail/html/subcopy.blade.php index 790ce6c..773cecc 100644 --- a/resources/views/vendor/mail/html/subcopy.blade.php +++ b/resources/views/vendor/mail/html/subcopy.blade.php @@ -1,7 +1 @@ - - - - - diff --git a/resources/views/vendor/mail/html/themes/default.css b/resources/views/vendor/mail/html/themes/default.css index 9265ab6..5046a45 100644 --- a/resources/views/vendor/mail/html/themes/default.css +++ b/resources/views/vendor/mail/html/themes/default.css @@ -10,8 +10,8 @@ body *:not(html):not(style):not(br):not(tr):not(code) { body { -webkit-text-size-adjust: none; - background-color: #ffffff; - color: #718096; + background-color: #eee; + color: #444; height: 100%; line-height: 1.4; margin: 0; @@ -31,7 +31,7 @@ a { color: #3869d4; } -a img { +img { border: none; } @@ -39,10 +39,11 @@ a img { h1 { color: #3d4852; - font-size: 24px; - font-weight: bold; - margin-top: 14px; - text-align: center; + font-size: 12px; + margin: 0; + padding: 0; + display: inline-block; + border: 1px solid red; } h2 { @@ -59,9 +60,17 @@ h3 { text-align: left; } +h4 { + font-size: 12px; + font-weight: bold; + margin-top: 0; + margin-bottom: 0; + text-align: left; +} + p { - font-size: 16px; - line-height: 1.5em; + font-size: 14px; + line-height: 1.2em; margin-top: 0; text-align: left; } @@ -88,9 +97,9 @@ hr { -premailer-cellpadding: 0; -premailer-cellspacing: 0; -premailer-width: 100%; - background-color: #edf2f7; - margin: 0; - padding: 0; + background-color: #eee; + margin: 30px auto; + padding: 12px; width: 100%; } @@ -107,23 +116,21 @@ hr { text-align: center; } -.narrow { - padding-left: 32px; - padding-right: 32px; +.tall { + padding-top: 16px; + padding-bottom: 16px; } /* Header */ .header { - padding: 25px 0; - text-align: center; + padding-top: 28px; + padding-bottom: 32px; } .header a { - color: #3d4852; - font-size: 19px; - font-weight: bold; text-decoration: none; + display: inline-block; } /* Logo */ @@ -154,24 +161,13 @@ hr { -premailer-width: 570px; background-color: #ffffff; border-color: #e8e5ef; - border-radius: 2px; + border-radius: 8px; border-width: 1px; - box-shadow: 0 2px 0 rgba(0, 0, 150, 0.025), 2px 4px 0 rgba(0, 0, 150, 0.015); + box-shadow: 4px 4px 12px rgba(0, 0, 0, 0.05); margin: 0 auto; - padding: 0; + padding: 0 0 32px; width: 570px; -} - -/* Subcopy */ - -.subcopy { - border-top: 1px solid #e8e5ef; - margin-top: 25px; - padding-top: 25px; -} - -.subcopy p { - font-size: 14px; + max-width: 570px; } /* Footer */ @@ -181,7 +177,7 @@ hr { -premailer-cellspacing: 0; -premailer-width: 570px; margin: 0 auto; - padding: 0; + padding: 32px 0 0; text-align: center; width: 570px; } @@ -204,7 +200,7 @@ hr { -premailer-cellpadding: 0; -premailer-cellspacing: 0; -premailer-width: 100%; - margin: 30px auto; + margin: 0; width: 100%; } @@ -223,8 +219,9 @@ hr { } .content-cell { - max-width: 100vw; - padding: 32px; + max-width: 570px; + padding-left: 32px; + padding-right: 32px; } /* Buttons */ @@ -246,10 +243,6 @@ hr { display: inline-block; overflow: hidden; text-decoration: none; -} - -.button-blue, -.button-primary { background-color: #2d3748; border-bottom: 8px solid #2d3748; border-left: 18px solid #2d3748; @@ -257,6 +250,15 @@ hr { border-top: 8px solid #2d3748; } +.button-blue, +.button-primary { + background-color: #0284C7; + border-bottom: 8px solid #0284C7; + border-left: 18px solid #0284C7; + border-right: 18px solid #0284C7; + border-top: 8px solid #0284C7; +} + .button-green, .button-success { background-color: #48bb78; diff --git a/resources/views/vendor/mail/text/button.blade.php b/resources/views/vendor/mail/text/button.blade.php index 97444eb..30fcb5f 100644 --- a/resources/views/vendor/mail/text/button.blade.php +++ b/resources/views/vendor/mail/text/button.blade.php @@ -1 +1 @@ -{{ $slot }}: {{ $url }} +{{ $url }} diff --git a/resources/views/vendor/mail/text/header.blade.php b/resources/views/vendor/mail/text/header.blade.php index 97444eb..3338f62 100644 --- a/resources/views/vendor/mail/text/header.blade.php +++ b/resources/views/vendor/mail/text/header.blade.php @@ -1 +1 @@ -{{ $slot }}: {{ $url }} +{{ $slot }} diff --git a/resources/views/vendor/mail/text/layout.blade.php b/resources/views/vendor/mail/text/layout.blade.php index ec58e83..1e4bd2c 100644 --- a/resources/views/vendor/mail/text/layout.blade.php +++ b/resources/views/vendor/mail/text/layout.blade.php @@ -1,9 +1,21 @@ {!! strip_tags($header ?? '') !!} -{!! strip_tags($slot) !!} -@isset($subcopy) +@php +$slot = str_replace([' ', "\t"], '', $slot); +$slot = str_replace('

', "\r\n", $slot); +$slot = strip_tags($slot); +@endphp +{!! $slot !!} -{!! strip_tags($subcopy) !!} +@isset($subcopy) +@php + $subcopy = str_replace([' ', "\t"], '', $subcopy); + $subcopy = str_replace("\n", " - ", $subcopy); + $subcopy = str_replace(['
', '
', '

'], "\r\n", $subcopy); + $subcopy = strip_tags($subcopy); +@endphp +{!! $subcopy !!} @endisset +------ {!! strip_tags($footer ?? '') !!} diff --git a/resources/views/vendor/mail/text/message.blade.php b/resources/views/vendor/mail/text/message.blade.php index 80bce21..2205462 100644 --- a/resources/views/vendor/mail/text/message.blade.php +++ b/resources/views/vendor/mail/text/message.blade.php @@ -1,27 +1,24 @@ - {{-- Header --}} - - - {{ config('app.name') }} - - +{{-- Body --}} +{{ $slot }} - {{-- Body --}} - {{ $slot }} +@isset($subcopy) + +{{ $subcopy }} + +@endisset - {{-- Subcopy --}} - @isset($subcopy) - - - {{ $subcopy }} - - - @endisset - {{-- Footer --}} - - - © {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.') - - +{{-- Footer --}} + + + +This email was sent to {{ $email }} + +STEMMechanics | 1/4 Jordan Street | Edmonton, QLD 4869 Australia +© {{ date('Y') }} {{ config('app.name') }}. {{ __('All rights reserved.') }} + +@isset($unsubscribe) Unsubscribe: {{ $unsubscribe }}@endisset + + diff --git a/resources/views/vendor/mail/text/subcopy.blade.php b/resources/views/vendor/mail/text/subcopy.blade.php index 3338f62..2dd8cc9 100644 --- a/resources/views/vendor/mail/text/subcopy.blade.php +++ b/resources/views/vendor/mail/text/subcopy.blade.php @@ -1 +1,2 @@ + {{ $slot }} diff --git a/routes/console.php b/routes/console.php index 0114338..7fb3cf0 100644 --- a/routes/console.php +++ b/routes/console.php @@ -8,13 +8,8 @@ use Illuminate\Support\Facades\Storage; Artisan::command('cleanup', function() { // Clean up expired tokens - DB::table('login_tokens') - ->where('created_at', '<', now()->subMinutes(10)) - ->delete(); - - // Clean up expired change email requests - DB::table('email_updates') - ->where('created_at', '<', now()->subMinutes(10)) + DB::table('tokens') + ->where('expires_at', '<', now()) ->delete(); // Published scheduled posts