Skip to content

Commit 0527c02

Browse files
authored
fix(plugin-auth): reduce password rate limit aggressiveness and reset limits on password change (#877)
1 parent 335e063 commit 0527c02

33 files changed

Lines changed: 410 additions & 147 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@baseplate-dev/plugin-auth': patch
3+
---
4+
5+
Reduce password rate limit aggressiveness (15 attempts/hour for IP, 10 consecutive fails/hour), reset login rate limits after successful password reset, and improve error messages to suggest password reset when rate limited
Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
import type React from 'react';
12
import type { ReactElement } from 'react';
23

34
import { Outlet } from '@tanstack/react-router';
45

56
import { AsyncBoundary } from '../ui/async-boundary';
67
import { Separator } from '../ui/separator';
7-
import { SidebarProvider, SidebarTrigger } from '../ui/sidebar';
8+
import { SidebarInset, SidebarProvider, SidebarTrigger } from '../ui/sidebar';
89
import { AppBreadcrumbs } from './app-breadcrumbs';
910
import { AppSidebar } from './app-sidebar';
1011

@@ -14,23 +15,37 @@ interface Props {
1415

1516
export function AdminLayout({ className }: Props): ReactElement {
1617
return (
17-
<SidebarProvider className={className}>
18+
<SidebarProvider
19+
className={className}
20+
style={
21+
{
22+
'--sidebar-width': 'calc(var(--spacing) * 72)',
23+
'--header-height': 'calc(var(--spacing) * 12)',
24+
} as React.CSSProperties
25+
}
26+
>
1827
<AppSidebar />
19-
<div className="flex h-full w-full flex-col">
20-
<header className="flex h-16 items-center gap-2 px-6">
21-
<SidebarTrigger />
22-
<Separator
23-
orientation="vertical"
24-
className="mr-2 data-[orientation=vertical]:h-4"
25-
/>
26-
<AppBreadcrumbs />
28+
<SidebarInset>
29+
<header className="flex h-(--header-height) shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
30+
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
31+
<SidebarTrigger className="-ml-1" />
32+
<Separator
33+
orientation="vertical"
34+
className="mx-2 data-[orientation=vertical]:h-4 data-[orientation=vertical]:self-center"
35+
/>
36+
<AppBreadcrumbs />
37+
</div>
2738
</header>
28-
<main className="flex-1 p-4">
29-
<AsyncBoundary>
30-
<Outlet />
31-
</AsyncBoundary>
32-
</main>
33-
</div>
39+
<div className="flex flex-1 flex-col">
40+
<div className="@container/main flex flex-1 flex-col gap-2">
41+
<main className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
42+
<AsyncBoundary>
43+
<Outlet />
44+
</AsyncBoundary>
45+
</main>
46+
</div>
47+
</div>
48+
</SidebarInset>
3449
</SidebarProvider>
3550
);
3651
}

examples/blog-with-auth/apps/admin/baseplate/generated/src/routes/admin/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ function HomePage(): ReactElement {
2929
}
3030

3131
return (
32-
<div className="space-y-4">
32+
<div className="space-y-4 p-4">
3333
<p>Welcome {data.viewer?.email ?? 'an anonymous user'}!</p>
3434
</div>
3535
);

examples/blog-with-auth/apps/admin/baseplate/generated/src/routes/auth_/forgot-password.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export const Route = createFileRoute('/auth_/forgot-password')({
3131

3232
const formSchema = z.object({
3333
email: z
34-
.email()
34+
.email('Please enter a valid email address')
3535
.max(PASSWORD_MAX_LENGTH)
3636
.transform((value) => value.toLowerCase()),
3737
});

examples/blog-with-auth/apps/admin/baseplate/generated/src/routes/auth_/login.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,16 @@ export const Route = createFileRoute('/auth_/login')({
4343
});
4444

4545
const formSchema = z.object({
46-
email: z.email().transform((value) => value.toLowerCase()),
47-
password: z.string().min(PASSWORD_MIN_LENGTH).max(PASSWORD_MAX_LENGTH),
46+
email: z
47+
.email('Please enter a valid email address')
48+
.transform((value) => value.toLowerCase()),
49+
password: z
50+
.string()
51+
.min(
52+
PASSWORD_MIN_LENGTH,
53+
`Password must be at least ${PASSWORD_MIN_LENGTH} characters`,
54+
)
55+
.max(PASSWORD_MAX_LENGTH),
4856
});
4957

5058
type FormData = z.infer<typeof formSchema>;
@@ -96,6 +104,8 @@ function LoginPage(): React.JSX.Element {
96104
.catch((err: unknown) => {
97105
const errorCode = getApolloErrorCode(err, [
98106
'invalid-credentials',
107+
'login-ip-rate-limited',
108+
'login-consecutive-fails-blocked',
99109
] as const);
100110
switch (errorCode) {
101111
case 'invalid-credentials': {
@@ -107,6 +117,15 @@ function LoginPage(): React.JSX.Element {
107117
);
108118
break;
109119
}
120+
case 'login-ip-rate-limited':
121+
case 'login-consecutive-fails-blocked': {
122+
resetField('password');
123+
setFormError('password', {
124+
message:
125+
'Too many failed login attempts. Please reset your password or try again later.',
126+
});
127+
break;
128+
}
110129
default: {
111130
toast.error(
112131
logAndFormatError(err, 'Sorry, we could not log you in.'),

examples/blog-with-auth/apps/admin/baseplate/generated/src/routes/auth_/register.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,17 @@ export const Route = createFileRoute('/auth_/register')({
4343
});
4444

4545
const formSchema = /* TPL_REGISTER_SCHEMA:START */ z.object({
46-
email: z.email().transform((value) => value.toLowerCase()),
47-
name: z.string().min(1).max(100),
48-
password: z.string().min(PASSWORD_MIN_LENGTH).max(PASSWORD_MAX_LENGTH),
46+
email: z
47+
.email('Please enter a valid email address')
48+
.transform((value) => value.toLowerCase()),
49+
name: z.string().min(1, 'Please enter your name').max(100),
50+
password: z
51+
.string()
52+
.min(
53+
PASSWORD_MIN_LENGTH,
54+
`Password must be at least ${PASSWORD_MIN_LENGTH} characters`,
55+
)
56+
.max(PASSWORD_MAX_LENGTH),
4957
}); /* TPL_REGISTER_SCHEMA:END */
5058

5159
type FormData = z.infer<typeof formSchema>;

examples/blog-with-auth/apps/admin/baseplate/generated/src/routes/auth_/reset-password.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,19 @@ export const Route = createFileRoute('/auth_/reset-password')({
4444

4545
const formSchema = z
4646
.object({
47-
newPassword: z.string().min(PASSWORD_MIN_LENGTH).max(PASSWORD_MAX_LENGTH),
47+
newPassword: z
48+
.string()
49+
.min(
50+
PASSWORD_MIN_LENGTH,
51+
`Password must be at least ${PASSWORD_MIN_LENGTH} characters`,
52+
)
53+
.max(PASSWORD_MAX_LENGTH),
4854
confirmPassword: z
4955
.string()
50-
.min(PASSWORD_MIN_LENGTH)
56+
.min(
57+
PASSWORD_MIN_LENGTH,
58+
`Password must be at least ${PASSWORD_MIN_LENGTH} characters`,
59+
)
5160
.max(PASSWORD_MAX_LENGTH),
5261
})
5362
.refine((data) => data.newPassword === data.confirmPassword, {
Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
import type React from 'react';
12
import type { ReactElement } from 'react';
23

34
import { Outlet } from '@tanstack/react-router';
45

56
import { AsyncBoundary } from '../ui/async-boundary';
67
import { Separator } from '../ui/separator';
7-
import { SidebarProvider, SidebarTrigger } from '../ui/sidebar';
8+
import { SidebarInset, SidebarProvider, SidebarTrigger } from '../ui/sidebar';
89
import { AppBreadcrumbs } from './app-breadcrumbs';
910
import { AppSidebar } from './app-sidebar';
1011

@@ -14,23 +15,37 @@ interface Props {
1415

1516
export function AdminLayout({ className }: Props): ReactElement {
1617
return (
17-
<SidebarProvider className={className}>
18+
<SidebarProvider
19+
className={className}
20+
style={
21+
{
22+
'--sidebar-width': 'calc(var(--spacing) * 72)',
23+
'--header-height': 'calc(var(--spacing) * 12)',
24+
} as React.CSSProperties
25+
}
26+
>
1827
<AppSidebar />
19-
<div className="flex h-full w-full flex-col">
20-
<header className="flex h-16 items-center gap-2 px-6">
21-
<SidebarTrigger />
22-
<Separator
23-
orientation="vertical"
24-
className="mr-2 data-[orientation=vertical]:h-4"
25-
/>
26-
<AppBreadcrumbs />
28+
<SidebarInset>
29+
<header className="flex h-(--header-height) shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
30+
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
31+
<SidebarTrigger className="-ml-1" />
32+
<Separator
33+
orientation="vertical"
34+
className="mx-2 data-[orientation=vertical]:h-4 data-[orientation=vertical]:self-center"
35+
/>
36+
<AppBreadcrumbs />
37+
</div>
2738
</header>
28-
<main className="flex-1 p-4">
29-
<AsyncBoundary>
30-
<Outlet />
31-
</AsyncBoundary>
32-
</main>
33-
</div>
39+
<div className="flex flex-1 flex-col">
40+
<div className="@container/main flex flex-1 flex-col gap-2">
41+
<main className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
42+
<AsyncBoundary>
43+
<Outlet />
44+
</AsyncBoundary>
45+
</main>
46+
</div>
47+
</div>
48+
</SidebarInset>
3449
</SidebarProvider>
3550
);
3651
}

examples/blog-with-auth/apps/admin/src/routes/admin/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ function HomePage(): ReactElement {
2929
}
3030

3131
return (
32-
<div className="space-y-4">
32+
<div className="space-y-4 p-4">
3333
<p>Welcome {data.viewer?.email ?? 'an anonymous user'}!</p>
3434
</div>
3535
);

examples/blog-with-auth/apps/admin/src/routes/auth_/forgot-password.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export const Route = createFileRoute('/auth_/forgot-password')({
3131

3232
const formSchema = z.object({
3333
email: z
34-
.email()
34+
.email('Please enter a valid email address')
3535
.max(PASSWORD_MAX_LENGTH)
3636
.transform((value) => value.toLowerCase()),
3737
});

0 commit comments

Comments
 (0)