Skip to content

Commit 2cf611a

Browse files
authored
chore: disable password reset flow in self hosted (#235)
1 parent 9bfc2eb commit 2cf611a

6 files changed

Lines changed: 122 additions & 81 deletions

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"use client";
2+
3+
import Client from "@blink.so/api";
4+
import { useMemo, useState } from "react";
5+
import { Button } from "@/components/ui/button";
6+
import { Input } from "@/components/ui/input";
7+
8+
export default function ResetPasswordForm() {
9+
const client = useMemo(() => new Client(), []);
10+
const [error, setError] = useState<string | undefined>();
11+
const [isSubmitting, setIsSubmitting] = useState(false);
12+
13+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
14+
e.preventDefault();
15+
setError(undefined);
16+
17+
const formData = new FormData(e.currentTarget);
18+
const email = formData.get("email") as string;
19+
20+
if (!email) {
21+
setError("Please enter your email address");
22+
return;
23+
}
24+
25+
setIsSubmitting(true);
26+
try {
27+
const result = await client.auth.requestPasswordReset({ email });
28+
if (result.ok && result.redirect_url) {
29+
window.location.href = result.redirect_url;
30+
}
31+
} catch (err) {
32+
setError(
33+
err instanceof Error ? err.message : "Failed to request password reset"
34+
);
35+
setIsSubmitting(false);
36+
}
37+
};
38+
39+
return (
40+
<div className="space-y-4">
41+
{error && (
42+
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
43+
<p className="text-sm text-red-800 dark:text-red-200">{error}</p>
44+
</div>
45+
)}
46+
47+
<form className="space-y-4" onSubmit={handleSubmit}>
48+
<div>
49+
<label
50+
htmlFor="email"
51+
className="block text-sm font-medium text-zinc-900 dark:text-white mb-2"
52+
>
53+
Email
54+
</label>
55+
<Input
56+
id="email"
57+
name="email"
58+
type="email"
59+
required
60+
placeholder="Enter your email address"
61+
className="w-full"
62+
data-testid="reset-email-input"
63+
/>
64+
</div>
65+
66+
<Button
67+
type="submit"
68+
size="lg"
69+
className="w-full h-12 text-base font-medium"
70+
data-testid="reset-email-submit"
71+
disabled={isSubmitting}
72+
>
73+
{isSubmitting ? "Sending..." : "Send Reset Code"}
74+
</Button>
75+
</form>
76+
</div>
77+
);
78+
}

internal/site/app/(public)/reset-password/page.stories.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import Layout from "@/app/(public)/layout";
21
import type { Meta, StoryObj } from "@storybook/react";
2+
import { mocked } from "storybook/test";
3+
import Layout from "@/app/(public)/layout";
4+
import { getEmailDeliveryConfigured } from "@/lib/email-delivery.mock";
35
import ResetPasswordPage from "./page";
46

57
const meta: Meta = {
@@ -12,8 +14,8 @@ const meta: Meta = {
1214
</Layout>
1315
),
1416
],
15-
args: {
16-
searchParams: Promise.resolve({}),
17+
beforeEach: () => {
18+
mocked(getEmailDeliveryConfigured).mockReturnValue(true);
1719
},
1820
};
1921

@@ -23,3 +25,10 @@ type Story = StoryObj<typeof meta>;
2325
export const Default: Story = {
2426
args: {},
2527
};
28+
29+
export const EmailDeliveryDisabled: Story = {
30+
args: {},
31+
beforeEach: () => {
32+
mocked(getEmailDeliveryConfigured).mockReturnValue(false);
33+
},
34+
};

internal/site/app/(public)/reset-password/page.tsx

Lines changed: 25 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,35 @@
1-
"use client";
1+
import { getEmailDeliveryConfigured } from "@/lib/email-delivery";
2+
import ResetPasswordForm from "./form";
23

3-
import { Button } from "@/components/ui/button";
4-
import { Input } from "@/components/ui/input";
5-
import Client from "@blink.so/api";
6-
import { useMemo, useState } from "react";
4+
export const dynamic = "force-dynamic";
75

86
export default function ResetPasswordPage() {
9-
const client = useMemo(() => new Client(), []);
10-
const [error, setError] = useState<string | undefined>();
11-
const [isSubmitting, setIsSubmitting] = useState(false);
7+
const isEmailConfigured = getEmailDeliveryConfigured();
128

13-
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
14-
e.preventDefault();
15-
setError(undefined);
16-
17-
const formData = new FormData(e.currentTarget);
18-
const email = formData.get("email") as string;
19-
20-
if (!email) {
21-
setError("Please enter your email address");
22-
return;
23-
}
24-
25-
setIsSubmitting(true);
26-
try {
27-
const result = await client.auth.requestPasswordReset({ email });
28-
if (result.ok && result.redirect_url) {
29-
window.location.href = result.redirect_url;
30-
}
31-
} catch (err) {
32-
setError(
33-
err instanceof Error ? err.message : "Failed to request password reset"
34-
);
35-
setIsSubmitting(false);
36-
}
37-
};
9+
if (!isEmailConfigured) {
10+
return (
11+
<div className="flex min-h-[60vh] items-center justify-center p-4">
12+
<div className="text-center">
13+
<h1 className="text-2xl font-semibold text-zinc-900 dark:text-white mb-2">
14+
Contact this instance's admin to reset your password
15+
</h1>
16+
<p className="text-sm text-neutral-600 dark:text-neutral-400">
17+
Remember your password?{" "}
18+
<a
19+
href="/login"
20+
className="text-blue-600 dark:text-blue-400 hover:underline font-medium"
21+
>
22+
Sign in
23+
</a>
24+
</p>
25+
</div>
26+
</div>
27+
);
28+
}
3829

3930
return (
4031
<div className="flex items-center justify-center p-4">
4132
<div className="w-full max-w-md">
42-
{/* Header */}
4333
<div className="text-center mb-8">
4434
<h1 className="text-3xl font-bold text-zinc-900 dark:text-white mb-2">
4535
Reset your password
@@ -50,51 +40,9 @@ export default function ResetPasswordPage() {
5040
</p>
5141
</div>
5242

53-
{/* Reset Password Card */}
5443
<div className="bg-white dark:bg-zinc-900 rounded-2xl border border-zinc-200 dark:border-zinc-700 shadow-lg p-8">
55-
<div className="space-y-4">
56-
{/* Error Display */}
57-
{error && (
58-
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
59-
<p className="text-sm text-red-800 dark:text-red-200">
60-
{error}
61-
</p>
62-
</div>
63-
)}
64-
65-
{/* Reset Password Form */}
66-
<form className="space-y-4" onSubmit={handleSubmit}>
67-
<div>
68-
<label
69-
htmlFor="email"
70-
className="block text-sm font-medium text-zinc-900 dark:text-white mb-2"
71-
>
72-
Email
73-
</label>
74-
<Input
75-
id="email"
76-
name="email"
77-
type="email"
78-
required
79-
placeholder="Enter your email address"
80-
className="w-full"
81-
data-testid="reset-email-input"
82-
/>
83-
</div>
84-
85-
<Button
86-
type="submit"
87-
size="lg"
88-
className="w-full h-12 text-base font-medium"
89-
data-testid="reset-email-submit"
90-
disabled={isSubmitting}
91-
>
92-
{isSubmitting ? "Sending..." : "Send Reset Code"}
93-
</Button>
94-
</form>
95-
</div>
44+
<ResetPasswordForm />
9645

97-
{/* Footer */}
9846
<div className="mt-6 text-center">
9947
<p className="text-sm text-neutral-600 dark:text-neutral-400">
10048
Remember your password?{" "}
@@ -108,7 +56,6 @@ export default function ResetPasswordPage() {
10856
</div>
10957
</div>
11058

111-
{/* Terms */}
11259
<div className="mt-6 text-center">
11360
<p className="text-xs text-neutral-500">
11461
Need help? Contact our{" "}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { fn } from "storybook/test";
2+
3+
export const getEmailDeliveryConfigured = fn<() => boolean>(() => true);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const getEmailDeliveryConfigured = (): boolean => {
2+
return process.env.BLINK_EMAIL_DELIVERY_CONFIGURED === "true";
3+
};

packages/server/src/server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,7 @@ const startNextServer = async (opts: StartNextServerOptions) => {
669669
process.env.AUTH_SECRET = opts.authSecret;
670670
process.env.NEXT_PUBLIC_BASE_URL = opts.baseUrl;
671671
process.env.BLINK_ENABLE_SIGNUPS = opts.enableSignups ? "true" : "false";
672+
process.env.BLINK_EMAIL_DELIVERY_CONFIGURED = "false";
672673

673674
let nextConfig: any = {};
674675
try {

0 commit comments

Comments
 (0)