Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion resources/js/common/layouts/AuthBase.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,20 @@
import craftCmsLogoUrl from '@public/images/craftcms.svg';
import {t} from '@craftcms/cp';

const props = withDefaults(
defineProps<{
title?: string;
}>(),
{
title: t('Sign In'),
}
);

const {general, system} = useCraftData();
</script>

<template>
<Head :title="t('Sign In')"></Head>
<Head :title="props.title"></Head>
<main class="cp-login">
<div class="cp-login__wrapper grid gap-3 justify-items-center">
<h1 class="flex justify-center">
Expand Down
69 changes: 69 additions & 0 deletions resources/js/pages/auth/SetPassword.vue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally this would be a web component (like login, totp and recovery-codes). My thinking there is that we could expose them to end users who could include those web components in whatever template they want as a more robust way to customize the login flow and pages

@riasvdv riasvdv Jun 28, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've moved them to web components, also updated the Twig fallback views (which are used on the site routes) to use those components.

The components themselves now have styles though, which people might not want when using them in their own implementations? The login components do as well so I've followed that for now

Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<script setup lang="ts">
import {computed} from 'vue';
import {t} from '@craftcms/cp';
import {useForm} from '@inertiajs/vue3';
import AuthBase from '@/common/layouts/AuthBase.vue';
import Pane from '@/common/components/Pane.vue';
import CraftInputPassword from '@craftcms/cp/vue/CraftInputPassword.vue';
import {store} from '@actions/Auth/SetPasswordController';

interface SetPasswordForm {
uid: string;
code: string;
newPassword: string;
}

const props = defineProps<{
uid: string;
code: string;
newUser: boolean;
}>();

const form = useForm<SetPasswordForm>({
uid: props.uid,
code: props.code,
newPassword: '',
});

const title = computed(() =>
props.newUser ? t('Set Your Password') : t('Set Your New Password')
);

const passwordLabel = computed(() =>
props.newUser ? t('Choose a password') : t('Choose a new password')
);

function submit() {
form.post(store().url);
}
</script>

<template>
<AuthBase :title="title">
<form @submit.prevent="submit">
<Pane appearance="raised">
<div class="grid gap-3">
<CraftInputPassword
id="newPassword"
name="newPassword"
v-model="form.newPassword"
:label="passwordLabel"
:error="form.errors.newPassword"
autocomplete="new-password"
required
autofocus
/>

<craft-button
type="submit"
variant="accent"
:loading="form.processing"
class="w-full"
>
{{ t('Set Password') }}
</craft-button>
</div>
</Pane>
</form>
</AuthBase>
</template>
49 changes: 49 additions & 0 deletions resources/js/pages/auth/VerifyEmail.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<script setup lang="ts">
import {t} from '@craftcms/cp';
import {useForm} from '@inertiajs/vue3';
import AuthBase from '@/common/layouts/AuthBase.vue';
import Pane from '@/common/components/Pane.vue';
import {store} from '@actions/Auth/VerifyEmailController';

interface VerifyEmailForm {
uid: string;
code: string;
}

const props = defineProps<{
uid: string;
code: string;
}>();

const form = useForm<VerifyEmailForm>({
uid: props.uid,
code: props.code,
});

function submit() {
form.post(store().url);
}
</script>

<template>
<AuthBase :title="t('Verify your email address')">
<form @submit.prevent="submit">
<Pane appearance="raised">
<div class="grid gap-3">
<h2 class="text-base">
{{ t('Verify your email address') }}
</h2>

<craft-button
type="submit"
variant="accent"
:loading="form.processing"
class="w-full"
>
{{ t('Verify') }}
</craft-button>
</div>
</Pane>
</form>
</AuthBase>
</template>
2 changes: 1 addition & 1 deletion resources/templates/set-password.twig
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
{% block message %}
<form method="post" accept-charset="UTF-8">
{{ hiddenInput('code', code) }}
{{ hiddenInput('uid', id) }}
{{ hiddenInput('uid', uid) }}
{{ csrfInput() }}

<div>
Expand Down
2 changes: 1 addition & 1 deletion resources/templates/verify-email.twig
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

{% block message %}
<form method="post" accept-charset="UTF-8">
{{ hiddenInput('uid', id) }}
{{ hiddenInput('uid', uid) }}
{{ hiddenInput('code', code) }}
{{ csrfInput() }}

Expand Down
6 changes: 6 additions & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use CraftCms\Cms\Edition;
use CraftCms\Cms\Http\Controllers\Auth\LoginController;
use CraftCms\Cms\Http\Controllers\Auth\OAuthController;
use CraftCms\Cms\Http\Controllers\Auth\SetPasswordController;
use CraftCms\Cms\Http\Controllers\Auth\TwoFactorAuthenticationController;
use CraftCms\Cms\Http\Controllers\Auth\VerifyEmailController;
use CraftCms\Cms\Http\Middleware\RequireEdition;
Expand All @@ -26,6 +27,11 @@
Route::post(Cms::config()->verifyEmailPath, [VerifyEmailController::class, 'store']);
}

if (Cms::config()->setPasswordPath !== false) {
Route::get(Cms::config()->setPasswordPath, [SetPasswordController::class, 'show']);
Route::post(Cms::config()->setPasswordPath, [SetPasswordController::class, 'store']);
}

Route::middleware('auth')->group(function () {
if (Cms::config()->logoutPath !== false) {
Route::get(Cms::config()->logoutPath, [LoginController::class, 'logout']);
Expand Down
3 changes: 2 additions & 1 deletion src/Config/GeneralConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace CraftCms\Cms\Config;

use Closure;
use CraftCms\Cms\Auth\Enums\CpAuthPath;
use CraftCms\Cms\Support\Attributes\EnvName;
use CraftCms\Cms\Support\Config as ConfigHelper;
use CraftCms\Cms\Support\DateTimeHelper;
Expand Down Expand Up @@ -2658,7 +2659,7 @@ class GeneralConfig extends BaseConfig
*
* @group Routing
*/
public mixed $setPasswordPath = 'setpassword';
public mixed $setPasswordPath = CpAuthPath::SetPassword->value;

/**
* @var mixed The URI to the page where users can request to change their password.
Expand Down
16 changes: 8 additions & 8 deletions src/Http/Controllers/Auth/AuthenticationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,16 +101,16 @@ protected function handleLoginFailure(Request $request, ?AuthError $authError =
return $this->asFailure($message, ['errorCode' => $authError?->value]);
}

protected function renderViewWithFallback(string $cpTemplate, array $data = [], ?string $inertiaComponent = null, ?array $inertiaProps = []): View|InertiaResponse|Response
protected function renderViewWithFallback(string $cpTemplate, array $data = [], ?string $inertiaComponent = null, ?array $inertiaProps = null): View|InertiaResponse|Response
{
if (view()->exists(request()->craftPath())) {
return view(request()->craftPath(), $data);
}

if ($inertiaComponent !== null && request()->isCpRequest()) {
return Inertia::render($inertiaComponent, $inertiaProps ?? $data);
}

if (view()->exists(request()->craftPath())) {
return view(request()->craftPath(), $data);
}

TemplateMode::set(TemplateMode::Cp);

if (! view()->exists('craftcms::'.$cpTemplate)) {
Expand All @@ -123,13 +123,13 @@ protected function renderViewWithFallback(string $cpTemplate, array $data = [],
protected function processTokenRequest(Request $request): Response|array
{
$request->validate([
'id' => ['required'],
'uid' => ['required'],
'code' => ['required'],
]);

/** @var User|null $user */
$user = User::find()
->uid($request->input('id'))
->uid($request->input('uid'))
->status(null)
->addSelect(['users.password'])
->one();
Expand All @@ -153,7 +153,7 @@ protected function processTokenRequest(Request $request): Response|array

event(new EmailVerified($user));

return [$user, $request->input('id'), $request->input('code')];
return [$user, $request->input('uid'), $request->input('code')];
}

protected function processInvalidToken(Request $request, ?User $user = null): Response
Expand Down
33 changes: 18 additions & 15 deletions src/Http/Controllers/Auth/SetPasswordController.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,16 @@
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password as PasswordFacade;
use Illuminate\Validation\Rules\Password;
use Illuminate\Validation\ValidationException;
use Inertia\Response as InertiaResponse;
use RuntimeException;
use Symfony\Component\HttpFoundation\Response;

use function CraftCms\Cms\t;

readonly class SetPasswordController extends AuthenticationController
{
public function show(Request $request, AuthMethods $auth): Response|View
public function show(Request $request, AuthMethods $auth): Response|View|InertiaResponse
{
if (! is_array($info = $this->processTokenRequest($request))) {
return $info;
Expand All @@ -42,28 +44,32 @@ public function show(Request $request, AuthMethods $auth): Response|View
$auth->setRememberedUsername($user);

// Send them to the set password template.
return $this->renderViewWithFallback('set-password', [
'code' => $code,
'id' => $uid,
'newUser' => ! $user->password,
]);
return $this->renderViewWithFallback(
cpTemplate: 'set-password',
data: [
'code' => $code,
'uid' => $uid,
'newUser' => ! $user->password,
],
inertiaComponent: 'auth/SetPassword',
);
}

public function store(Request $request, Users $users, Elements $elements): Response|View
public function store(Request $request, Users $users, Elements $elements): Response|View|InertiaResponse
{
$request->validate([
'code' => ['required'],
'id' => ['required'],
'uid' => ['required'],
'newPassword' => ['required', Password::default()],
]);

$user = User::find()
->uid($request->input('id'))
->uid($request->input('uid'))
->status(null)
->addSelect('users.password')
->first();

abort_if(is_null($user), 400, 'Invalid user UUID: '.$request->input('id'));
abort_if(is_null($user), 400, 'Invalid user UUID: '.$request->input('uid'));

try {
$status = PasswordFacade::broker()->reset(
Expand Down Expand Up @@ -98,11 +104,8 @@ function (CraftUser $authUser, string $password) use ($elements) {
);
}

return $this->renderViewWithFallback('set-password', [
'errors' => $user->errors()->get('newPassword'),
'code' => $request->input('code'),
'id' => $request->input('id'),
'newUser' => ! $user->password,
throw ValidationException::withMessages([
'newPassword' => $user->errors()->get('newPassword') ?: [t('Couldn’t update password.')],
]);
}

Expand Down
15 changes: 10 additions & 5 deletions src/Http/Controllers/Auth/VerifyEmailController.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@
use Illuminate\Contracts\View\View;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Inertia\Response as InertiaResponse;
use Symfony\Component\HttpFoundation\Response;

use function CraftCms\Cms\t;

readonly class VerifyEmailController extends AuthenticationController
{
public function show(Request $request): Response|View
public function show(Request $request): Response|View|InertiaResponse
{
if (! is_array($info = $this->processTokenRequest($request))) {
return $info;
Expand All @@ -33,10 +34,14 @@ public function show(Request $request): Response|View
app(AuthMethods::class)->setRememberedUsername($user);

// Send them to the set verify-email template
return $this->renderViewWithFallback('verify-email', [
'id' => $uid,
'code' => $code,
]);
return $this->renderViewWithFallback(
cpTemplate: 'verify-email',
data: [
'uid' => $uid,
'code' => $code,
],
inertiaComponent: 'auth/VerifyEmail',
);
}

public function store(Request $request, Users $users): Response|View
Expand Down
2 changes: 1 addition & 1 deletion src/User/Users.php
Original file line number Diff line number Diff line change
Expand Up @@ -1260,7 +1260,7 @@ private function getUserUrl(User $user, string $fePath, string $cpPath, ?string

$params = [
'code' => $token,
'id' => $user->uid,
'uid' => $user->uid,
];

$isCpRequest = request()->isCpRequest();
Expand Down
Loading
Loading