Skip to content

Commit 6e97e9d

Browse files
merge branch v2 (release 2.0.0-beta.27)
2 parents cc81a05 + d2facdb commit 6e97e9d

78 files changed

Lines changed: 2122 additions & 461 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,22 @@
11
# Changelog
22

3-
## [v2.0.0-beta.26](https://github.com/marcantondahmen/automad/commit/4c3fe37b5d5ae5bf299a317ade37b52f154c7c5c)
3+
## [v2.0.0-beta.27](https://github.com/marcantondahmen/automad/commit/3e26371c253034f7ca73d2fa470f1c75c0cb0a76)
44

5-
Mon, 6 Apr 2026 13:12:19 +0200
5+
Sun, 12 Apr 2026 20:28:19 +0200
6+
7+
### New Features
8+
9+
- add optional two factor authentication with totp ([64b1892f](https://github.com/marcantondahmen/automad/commit/64b1892f1787c0bf5ae4ecf26bdbf3aeb5d18e7d))
10+
- implement rate limiting for failed sign-in attempts ([fea5667a](https://github.com/marcantondahmen/automad/commit/fea5667a2100000a04bca19fcf8b502800092ac1))
11+
- improve account recovery flow and account settings ui ([a3bd987e](https://github.com/marcantondahmen/automad/commit/a3bd987e69b8ba8cc14ff2d5e49a8540d4820311))
12+
13+
### Bugfixes
14+
15+
- fix empty forms when closing a modal dialog on same page ([e54cd378](https://github.com/marcantondahmen/automad/commit/e54cd37852d0119f11f87339aaf431ea4b15b486))
16+
17+
## [v2.0.0-beta.26](https://github.com/marcantondahmen/automad/commit/cc81a0588c2ae9e7d445796320ac174fc82f8ca5)
18+
19+
Mon, 6 Apr 2026 13:44:34 +0200
620

721
### New Features
822

automad/lang/english.json

Lines changed: 103 additions & 71 deletions
Large diffs are not rendered by default.

automad/src/client/admin/components/Forms/ResetPasswordForm.ts renamed to automad/src/client/admin/components/Forms/AccountRecoveryForm.ts

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import { FormComponent } from './Form';
4141
*
4242
* @extends FormComponent
4343
*/
44-
export class ResetPasswordFormComponent extends FormComponent {
44+
export class AccountRecoveryFormComponent extends FormComponent {
4545
/**
4646
* The states object maps states to render methods.
4747
*/
@@ -85,11 +85,11 @@ export class ResetPasswordFormComponent extends FormComponent {
8585
*/
8686
private renderRequestToken(data: KeyValueMap): void {
8787
this.innerHTML = html`
88-
<h2>${App.text('resetPassword')}</h2>
88+
<h2>${App.text('accountRecoveryStartHeading')}</h2>
8989
<am-form-error></am-form-error>
9090
<div class="${CSS.card}">
9191
<div class="${CSS.cardBody} ${CSS.cardBodyLarge}">
92-
${App.text('passwordResetEnterNameOrEmail')}
92+
${App.text('accountRecoveryStartText')}
9393
</div>
9494
<div class="${CSS.cardForm}">
9595
<input
@@ -100,18 +100,17 @@ export class ResetPasswordFormComponent extends FormComponent {
100100
placeholder="${App.text('usernameOrEmail')}"
101101
/>
102102
<div class="${CSS.cardFormButtons}">
103-
<am-link
104-
class="${CSS.button}"
105-
${Attr.target}="${Route.login}"
106-
>
107-
${App.text('cancel')}
108-
</am-link>
109103
<am-submit class="${CSS.button} ${CSS.buttonPrimary}">
110-
${App.text('submit')}
104+
${App.text('accountRecoveryStartButton')}
111105
</am-submit>
112106
</div>
113107
</div>
114108
</div>
109+
<p>
110+
<am-link class="${CSS.link}" ${Attr.target}="${Route.login}">
111+
${App.text('accountRecoveryCancel')}
112+
</am-link>
113+
</p>
115114
`;
116115
}
117116

@@ -122,11 +121,11 @@ export class ResetPasswordFormComponent extends FormComponent {
122121
*/
123122
private renderSetPassword(data: KeyValueMap): void {
124123
this.innerHTML = html`
125-
<h2>${App.text('resetPassword')}</h2>
124+
<h2>${App.text('accountRecoveryNewPasswordHeading')}</h2>
126125
<am-form-error></am-form-error>
127126
<div class="${CSS.card}">
128127
<div class="${CSS.cardBody} ${CSS.cardBodyLarge}">
129-
${App.text('passwordResetEnterNewPassword')}
128+
${App.text('accountRecoveryNewPasswordText')}
130129
</div>
131130
<div class="${CSS.cardForm}">
132131
<input
@@ -165,18 +164,17 @@ export class ResetPasswordFormComponent extends FormComponent {
165164
value="${data.username}"
166165
/>
167166
<div class="${CSS.cardFormButtons}">
168-
<am-link
169-
class="${CSS.button}"
170-
${Attr.target}="${Route.login}"
171-
>
172-
${App.text('cancel')}
173-
</am-link>
174167
<am-submit class="${CSS.button} ${CSS.buttonPrimary}">
175168
${App.text('passwordResetSave')}
176169
</am-submit>
177170
</div>
178171
</div>
179172
</div>
173+
<p>
174+
<am-link class="${CSS.link}" ${Attr.target}="${Route.login}">
175+
${App.text('accountRecoveryCancel')}
176+
</am-link>
177+
</p>
180178
`;
181179
}
182180

@@ -187,10 +185,10 @@ export class ResetPasswordFormComponent extends FormComponent {
187185
*/
188186
private renderSuccess(data: KeyValueMap): void {
189187
this.innerHTML = html`
190-
<h2>${App.text('resetPassword')}</h2>
188+
<h2>${App.text('accountRecoverySuccessHeading')}</h2>
191189
<div class="${CSS.card}">
192190
<div class="${CSS.cardBody} ${CSS.cardBodyLarge}">
193-
${App.text('passwordChangedSuccess')}
191+
${App.text('accountRecoverySuccessText')}
194192
</div>
195193
<div class="${CSS.cardForm}">
196194
<am-link
@@ -205,4 +203,4 @@ export class ResetPasswordFormComponent extends FormComponent {
205203
}
206204
}
207205

208-
customElements.define('am-reset-password-form', ResetPasswordFormComponent);
206+
customElements.define('am-account-recovery-form', AccountRecoveryFormComponent);

automad/src/client/admin/components/Forms/DeleteUsersForm.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
* See LICENSE.md for license information.
3333
*/
3434

35-
import { App, CSS, EventName, html } from '@/admin/core';
35+
import { App, Attr, CSS, EventName, html } from '@/admin/core';
3636
import { FormComponent } from './Form';
3737

3838
/**
@@ -51,6 +51,17 @@ const renderRegisteredUsers = (): string => {
5151
`
5252
: html`<am-checkbox name="delete[${user.name}]"></am-checkbox>`;
5353

54+
const totpBadge = user.totpIsConfigured
55+
? html`
56+
<i
57+
class="bi bi-shield-check"
58+
${Attr.tooltip}="${App.text(
59+
'systemUsersTotpIsEnabled'
60+
)}"
61+
></i>
62+
`
63+
: '';
64+
5465
return html`
5566
${output}
5667
<span class="${CSS.card}">
@@ -61,7 +72,11 @@ const renderRegisteredUsers = (): string => {
6172
<span class="${CSS.flexItemGrow}">
6273
${user.name} (${user.email})
6374
</span>
64-
${checkbox}
75+
<span
76+
class="${CSS.flex} ${CSS.flexGap} ${CSS.flexAlignCenter}"
77+
>
78+
${totpBadge}${checkbox}
79+
</span>
6580
</span>
6681
</span>
6782
</span>

automad/src/client/admin/components/Forms/Form.ts

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,15 @@ import {
4343
getLogger,
4444
collectFieldData,
4545
getPageURL,
46-
notifyError,
4746
notifySuccess,
4847
query,
4948
queryAll,
5049
requestAPI,
5150
createProgressModal,
5251
EditLockController,
5352
getSlug,
53+
findFormErrorElement,
54+
notifyFormError,
5455
} from '@/admin/core';
5556
import {
5657
DeduplicationSettings,
@@ -60,7 +61,6 @@ import {
6061
import { BaseComponent } from '@/admin/components/Base';
6162
import { ModalComponent } from '@/admin/components/Modal/Modal';
6263
import { SubmitComponent } from './Submit';
63-
import { FormErrorComponent } from './FormError';
6464

6565
export const autoSubmitTimeout = 350;
6666

@@ -373,7 +373,7 @@ export class FormComponent extends BaseComponent {
373373
input.classList.add(CSS.validate);
374374
});
375375

376-
if (this.verifyRequired()) {
376+
if (this.validateInputs()) {
377377
queryAll(`.${CSS.validate}`, this).forEach((input) => {
378378
input.classList.remove(CSS.validate);
379379
});
@@ -430,24 +430,12 @@ export class FormComponent extends BaseComponent {
430430
App.reload();
431431
}
432432

433-
setTimeout(() => {
434-
const errorContainer = query<FormErrorComponent>(
435-
FormErrorComponent.TAG_NAME,
436-
this
437-
);
438-
439-
if (errorContainer) {
440-
errorContainer.message = error ?? '';
441-
}
442-
443-
if (error) {
444-
if (!errorContainer) {
445-
notifyError(error);
446-
}
447-
433+
if (error) {
434+
setTimeout(() => {
435+
notifyFormError(error, findFormErrorElement(this));
448436
getLogger().error(error);
449-
}
450-
}, 0);
437+
}, 0);
438+
}
451439

452440
if (success) {
453441
notifySuccess(success);
@@ -507,14 +495,21 @@ export class FormComponent extends BaseComponent {
507495
}
508496

509497
/**
510-
* Verifies that all required fields have values.
498+
* Verifies that all required fields have values and all fields are validated.
511499
*
512-
* @returns true if all required fields have values
500+
* @returns true if all required fields have values and all values are valid
513501
*/
514-
private verifyRequired(): boolean {
502+
private validateInputs(): boolean {
503+
const invalidInputs = queryAll<InputElement>(':invalid', this);
515504
const requiredInputs = queryAll<InputElement>('[required]', this);
516505
const requiredEmpty: InputElement[] = [];
517506

507+
if (invalidInputs.length) {
508+
invalidInputs[0].focus();
509+
510+
return false;
511+
}
512+
518513
requiredInputs.forEach((input) => {
519514
if (!input.value.trim()) {
520515
requiredEmpty.push(input);
@@ -523,6 +518,7 @@ export class FormComponent extends BaseComponent {
523518

524519
if (requiredEmpty.length) {
525520
requiredEmpty[0].focus();
521+
526522
return false;
527523
}
528524

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* ....
3+
* .: '':.
4+
* :::: ':..
5+
* ::. ''..
6+
* .:'.. ..':.:::' . :. '':.
7+
* :. '' '' '. ::::.. ..:
8+
* ::::. ..':.. .'''::::: .
9+
* :::::::.. '..:::: :. :::: :
10+
* ::'':::::::. ':::.'':.:::: :
11+
* :.. ''::::::....': '':: :
12+
* :::::. '::::: : .. '' .
13+
* .''::::::::... ':::.'' ..'' :.''''.
14+
* :..:::''::::: :::::...:'' :..:
15+
* ::::::. ':::: :::::::: ..:: .
16+
* ::::::::.:::: :::::::: :'':.:: .''
17+
* ::: '::::::::.' ''::::: :.' '': :
18+
* ::: :::::::::..' :::: ::...' .
19+
* ::: .:::::::::: :::: :::: .:'
20+
* '::' ''::::::: :::: : :: :
21+
* ':::: :::: :'' .:
22+
* :::: :::: ..''
23+
* :::: ..:::: .:''
24+
* '''' '''''
25+
*
26+
*
27+
* AUTOMAD
28+
*
29+
* Copyright (c) 2026 by Marc Anton Dahmen
30+
* https://marcdahmen.de
31+
*
32+
* See LICENSE.md for license information.
33+
*/
34+
35+
import { App, html } from '@/admin/core';
36+
import { BaseStateIndicatorComponent } from '../BaseStateIndicator';
37+
38+
/**
39+
* A user totp configured indicator.
40+
*
41+
* @extends BaseUpdateIndicatorComponent
42+
*/
43+
class UserTotpConfiguredIndicatorComponent extends BaseStateIndicatorComponent {
44+
/**
45+
* Render the state element.
46+
*/
47+
render(): void {
48+
const icon = App.user.totpIsConfigured
49+
? 'shield-fill-check'
50+
: 'shield-slash';
51+
52+
this.innerHTML = html`<i class="bi bi-${icon}"></i>`;
53+
}
54+
}
55+
56+
customElements.define(
57+
'am-user-totp-configured-indicator',
58+
UserTotpConfiguredIndicatorComponent
59+
);

automad/src/client/admin/components/Modal/Modal.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,14 +173,15 @@ export class ModalComponent extends BaseComponent {
173173
this.focusTrap.remove();
174174

175175
setTimeout(() => {
176-
queryAll(`.${CSS.validate}`).forEach((input) => {
176+
queryAll(`.${CSS.validate}`, this).forEach((input) => {
177177
input.classList.remove(CSS.validate);
178178
});
179179
}, 400);
180180

181181
if (this.hasAttribute(Attr.clearForm)) {
182182
queryAll<HTMLInputElement>(
183-
'input[type="text"], input[type="email"], input[type="password"], textarea'
183+
'input[type="text"], input[type="email"], input[type="password"], textarea',
184+
this
184185
).forEach((input) => {
185186
input.value = '';
186187
});

automad/src/client/admin/components/Pages/ResetPassword.ts renamed to automad/src/client/admin/components/Pages/AccountRecovery.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,12 @@ import { BaseCenteredLayoutComponent } from './BaseCenteredLayout';
4747
*
4848
* @extends BaseCenteredLayoutComponent
4949
*/
50-
export class ResetPasswordComponent extends BaseCenteredLayoutComponent {
50+
export class AccountRecoveryComponent extends BaseCenteredLayoutComponent {
5151
/**
5252
* Set the page title that is used a document title suffix.
5353
*/
5454
protected get pageTitle(): string {
55-
return App.text('resetPassword');
55+
return App.text('accountRecovery');
5656
}
5757

5858
/**
@@ -62,14 +62,14 @@ export class ResetPasswordComponent extends BaseCenteredLayoutComponent {
6262
*/
6363
protected renderMainPartial(): string {
6464
return html`
65-
<am-reset-password-form
66-
${Attr.api}="${UserController.resetPassword}"
67-
></am-reset-password-form>
65+
<am-account-recovery-form
66+
${Attr.api}="${UserController.accountRecovery}"
67+
></am-account-recovery-form>
6868
`;
6969
}
7070
}
7171

7272
customElements.define(
73-
getTagFromRoute(Route.resetpassword),
74-
ResetPasswordComponent
73+
getTagFromRoute(Route.accountrecovery),
74+
AccountRecoveryComponent
7575
);

0 commit comments

Comments
 (0)