Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions resources/js/legacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// elevated session modal can be called from pretty much anywhere
await Promise.all([
import('./modules/auth/components/login/login-form.js'),
import('./modules/auth/components/set-password/set-password-form.js'),
import('./modules/auth/components/verify-email/verify-email-form.js'),
import('./modules/auth/components/totp/totp-form.js'),
import('./modules/auth/components/recovery-codes/recovery-code-form.js'),
]);
Expand Down
12 changes: 9 additions & 3 deletions resources/js/modules/auth/components/login/login-form.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,23 @@ export default css`
place-items: center;
}
.login-form__fields {
.auth-form__fields {
display: flex;
gap: var(--c-spacing-md);
align-items: end;
}
.login-form__actions {
.auth-form__actions {
margin-block-start: var(--c-spacing-lg);
}
.login-form__error {
.auth-form__heading {
margin: 0;
font-size: var(--c-font-size);
font-weight: var(--c-font-weight-bold);
}
.auth-form__error {
margin-block-start: var(--c-spacing-md);
}
Expand Down
6 changes: 3 additions & 3 deletions resources/js/modules/auth/components/login/login-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ export default class CraftLoginForm extends LitElement {
return html`
<craft-pane>
<form
class="login-form"
class="auth-form"
method="post"
accept-charset="UTF-8"
@submit="${this.#onSubmit}"
Expand Down Expand Up @@ -316,7 +316,7 @@ export default class CraftLoginForm extends LitElement {
: nothing}
</craft-field-group>
<div class="login-form__actions">
<div class="auth-form__actions">
<craft-button
type="submit"
variant="accent"
Expand All @@ -329,7 +329,7 @@ export default class CraftLoginForm extends LitElement {
</form>
${this._error
? html`<craft-callout class="login-form__error" variant="danger"
? html`<craft-callout class="auth-form__error" variant="danger"
>${this._error}</craft-callout
>`
: nothing}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export default class CraftLoginResetPassword extends LitElement {
return html`
<craft-pane>
<form
class="login-form login-form--reset"
class="auth-form auth-form--reset"
method="post"
accept-charset="UTF-8"
@submit="${this.#onSubmit}"
Expand All @@ -95,7 +95,7 @@ export default class CraftLoginResetPassword extends LitElement {
</craft-input>
</craft-field-group>
<div class="login-form__actions">
<div class="auth-form__actions">
<craft-button
type="submit"
variant="accent"
Expand All @@ -106,7 +106,7 @@ export default class CraftLoginResetPassword extends LitElement {
</div>
${this._error
? html`<craft-callout variant="danger" class="login-form__error"
? html`<craft-callout variant="danger" class="auth-form__error"
>${this._error}</craft-callout
>`
: nothing}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {html, LitElement, nothing} from 'lit';
import {property, state} from 'lit/decorators.js';
import {t} from '@craftcms/cp';
import componentStyles from '../login/login-form.styles.js';

/**
* @summary Full-page set-password form.
* @since 6.0
*/
export default class CraftSetPasswordForm extends LitElement {
static override styles = [componentStyles];

@property() action = '';
@property() uid = '';
@property() code = '';
@property({attribute: 'initial-error'}) initialError = '';
@property({type: Boolean, attribute: 'new-user'}) newUser = false;

@state() private _busy = false;

#passwordLabel() {
return this.newUser ? t('Choose a password') : t('Choose a new password');
}

#onSubmit() {
this._busy = true;
}

override render() {
return html`
<craft-pane>
<form
class="auth-form"
method="post"
action="${this.action}"
accept-charset="UTF-8"
@submit="${this.#onSubmit}"
>
<input type="hidden" name="uid" value="${this.uid}" />
<input type="hidden" name="code" value="${this.code}" />
<craft-field-group>
<craft-input-password
label="${this.#passwordLabel()}"
id="newPassword"
name="newPassword"
autocomplete="new-password"
required
autofocus
></craft-input-password>
</craft-field-group>
<div class="auth-form__actions">
<craft-button
type="submit"
variant="accent"
?loading="${this._busy}"
style="width: 100%"
>
${t('Set Password')}
</craft-button>
</div>
</form>
${this.initialError
? html`<craft-callout class="auth-form__error" variant="danger"
>${this.initialError}</craft-callout
>`
: nothing}
</craft-pane>
`;
}
}

if (!customElements.get('craft-set-password-form')) {
customElements.define('craft-set-password-form', CraftSetPasswordForm);
}

declare global {
interface HTMLElementTagNameMap {
'craft-set-password-form': CraftSetPasswordForm;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {html, LitElement, nothing} from 'lit';
import {property, state} from 'lit/decorators.js';
import {t} from '@craftcms/cp';
import componentStyles from '../login/login-form.styles.js';

/**
* @summary Full-page verify-email form.
* @since 6.0
*/
export default class CraftVerifyEmailForm extends LitElement {
static override styles = [componentStyles];

@property() action = '';
@property() uid = '';
@property() code = '';
@property({attribute: 'initial-error'}) initialError = '';

@state() private _busy = false;

#onSubmit() {
this._busy = true;
}

override render() {
return html`
<craft-pane>
<form
class="auth-form"
method="post"
action="${this.action}"
accept-charset="UTF-8"
@submit="${this.#onSubmit}"
>
<input type="hidden" name="uid" value="${this.uid}" />
<input type="hidden" name="code" value="${this.code}" />
<h2 class="auth-form__heading">${t('Verify your email address')}</h2>
<div class="auth-form__actions">
<craft-button
type="submit"
variant="accent"
?loading="${this._busy}"
style="width: 100%"
>
${t('Verify')}
</craft-button>
</div>
</form>
${this.initialError
? html`<craft-callout class="auth-form__error" variant="danger"
>${this.initialError}</craft-callout
>`
: nothing}
</craft-pane>
`;
}
}

if (!customElements.get('craft-verify-email-form')) {
customElements.define('craft-verify-email-form', CraftVerifyEmailForm);
}

declare global {
interface HTMLElementTagNameMap {
'craft-verify-email-form': CraftVerifyEmailForm;
}
}
38 changes: 38 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,38 @@
<script setup lang="ts">
import {computed} from 'vue';
import {t} from '@craftcms/cp';
import {usePage} from '@inertiajs/vue3';
import AuthBase from '@/common/layouts/AuthBase.vue';
import {store} from '@actions/Auth/SetPasswordController';
import '@/modules/auth/components/set-password/set-password-form.js';

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

const page = usePage<{
errors?: Record<string, string>;
}>();

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

const action = computed(
() => store(undefined, {query: {uid: props.uid, code: props.code}}).url
);
</script>

<template>
<AuthBase :title="title">
<craft-set-password-form
:action="action"
:uid="uid"
:code="code"
:initial-error="page.props.errors?.newPassword"
:new-user="newUser ? '' : null"
></craft-set-password-form>
</AuthBase>
</template>
36 changes: 36 additions & 0 deletions resources/js/pages/auth/VerifyEmail.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script setup lang="ts">
import {t} from '@craftcms/cp';
import {computed} from 'vue';
import {usePage} from '@inertiajs/vue3';
import AuthBase from '@/common/layouts/AuthBase.vue';
import {store} from '@actions/Auth/VerifyEmailController';
import '@/modules/auth/components/verify-email/verify-email-form.js';

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

const page = usePage<{
errors?: Record<string, string>;
}>();

const action = computed(
() => store(undefined, {query: {uid: props.uid, code: props.code}}).url
);

const initialError = computed(
() => page.props.errors?.code ?? page.props.errors?.uid
);
</script>

<template>
<AuthBase :title="t('Verify your email address')">
<craft-verify-email-form
:action="action"
:uid="uid"
:code="code"
:initial-error="initialError"
></craft-verify-email-form>
</AuthBase>
</template>
33 changes: 7 additions & 26 deletions resources/templates/set-password.twig
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{% extends "_layouts/message" %}
{% import "_includes/forms" as forms %}

{% if newUser %}
{% set title = "Set Your Password"|t('app') %}
Expand All @@ -8,30 +7,12 @@
{% endif %}

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

<div>
{{ forms.passwordField({
id: 'newPassword',
label: (newUser ? "Choose a password"|t('app') : "Choose a new password"|t('app')),
name: 'newPassword',
autocomplete: 'new-password',
autofocus: true,
errors: errors.get('newPassword')
}) }}
</div>

<div class="buttons">
<button class="btn submit" type="submit">{{ 'Set Password'|t('app') }}</button>
</div>
</form>
<craft-set-password-form {{ attr({
uid: uid,
code: code,
'initial-error': passwordErrors ? passwordErrors|first : null,
'new-user': newUser,
}) }}></craft-set-password-form>
{% endblock %}

{% js %}
(() => {
new Craft.PasswordInput($('#newPassword'));
})();
{% endjs %}
Loading
Loading