Skip to content

Commit 9c5f28e

Browse files
add email code settings card
1 parent 7d414d1 commit 9c5f28e

4 files changed

Lines changed: 123 additions & 1 deletion

File tree

packages/web/src/app/(app)/settings/security/actions.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,41 @@ export const setCredentialsLoginEnabled = async (enabled: boolean): Promise<{ su
7171
)
7272
);
7373

74+
export const setEmailCodeLoginEnabled = async (enabled: boolean): Promise<{ success: boolean } | ServiceError> => sew(async () =>
75+
withAuth(async ({ org, role, prisma }) =>
76+
withMinimumOrgRole(role, OrgRole.OWNER, async () => {
77+
if (env.AUTH_EMAIL_CODE_LOGIN_ENABLED !== undefined) {
78+
return {
79+
statusCode: StatusCodes.BAD_REQUEST,
80+
errorCode: ErrorCode.EMAIL_CODE_LOGIN_CONTROLLED_BY_ENV,
81+
message: "Email code login is controlled by the AUTH_EMAIL_CODE_LOGIN_ENABLED environment variable and cannot be changed from the UI.",
82+
} satisfies ServiceError;
83+
}
84+
85+
const providers = await getProviders();
86+
const hasAlternativeLoginMethod = providers.some((provider) => provider.type !== "nodemailer");
87+
88+
// Don't allow disabling email code login when it would leave no other way to sign in.
89+
if (!enabled && !hasAlternativeLoginMethod) {
90+
return {
91+
statusCode: StatusCodes.BAD_REQUEST,
92+
errorCode: ErrorCode.EMAIL_CODE_LOGIN_CANNOT_BE_DISABLED,
93+
message: "Email code login cannot be disabled because no other login method is configured.",
94+
} satisfies ServiceError;
95+
}
96+
97+
await prisma.org.update({
98+
where: { id: org.id },
99+
data: { isEmailCodeLoginEnabled: enabled },
100+
});
101+
102+
return {
103+
success: true,
104+
};
105+
})
106+
)
107+
);
108+
74109
export const setAnonymousAccessStatus = async (enabled: boolean): Promise<ServiceError | boolean> => sew(async () =>
75110
withAuth(async ({ org, role, prisma }) =>
76111
withMinimumOrgRole(role, OrgRole.OWNER, async () => {
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"use client"
2+
3+
import { useState } from "react"
4+
import { Info } from "lucide-react"
5+
import { Switch } from "@/components/ui/switch"
6+
import { Alert, AlertDescription } from "@/components/ui/alert"
7+
import { setEmailCodeLoginEnabled } from "@/app/(app)/settings/security/actions"
8+
import { isServiceError } from "@/lib/utils"
9+
import { useToast } from "@/components/hooks/use-toast"
10+
import { BasicSettingsCard } from "@/app/(app)/settings/components/settingsCard"
11+
12+
interface EmailCodeLoginEnabledSettingsCardProps {
13+
isEmailCodeLoginEnabled: boolean
14+
isEmailServiceConfigured: boolean
15+
}
16+
17+
export function EmailCodeLoginEnabledSettingsCard({
18+
isEmailCodeLoginEnabled,
19+
isEmailServiceConfigured,
20+
}: EmailCodeLoginEnabledSettingsCardProps) {
21+
const [enabled, setEnabled] = useState(isEmailCodeLoginEnabled)
22+
const [isLoading, setIsLoading] = useState(false)
23+
const { toast } = useToast()
24+
25+
const handleToggle = async (checked: boolean) => {
26+
setIsLoading(true)
27+
try {
28+
const result = await setEmailCodeLoginEnabled(checked)
29+
30+
if (isServiceError(result)) {
31+
toast({
32+
title: "Error",
33+
description: result.message,
34+
variant: "destructive",
35+
})
36+
return
37+
}
38+
39+
setEnabled(checked)
40+
} catch (error) {
41+
console.error("Error updating email code login setting:", error)
42+
toast({
43+
title: "Error",
44+
description: "Failed to update email code login setting",
45+
variant: "destructive",
46+
})
47+
} finally {
48+
setIsLoading(false)
49+
}
50+
}
51+
52+
return (
53+
<BasicSettingsCard
54+
name="Email code login"
55+
description="When enabled, users can sign in with a one-time code sent to their email."
56+
footer={!isEmailServiceConfigured && (
57+
<Alert className="mt-4 items-center">
58+
<Info className="w-4 h-4 text-muted-foreground" />
59+
<AlertDescription>
60+
This setting requires transactional email to be configured.{" "}
61+
<a
62+
href="https://docs.sourcebot.dev/docs/configuration/transactional-emails"
63+
target="_blank"
64+
rel="noopener"
65+
className="underline text-primary hover:text-primary/80 transition-colors"
66+
>
67+
Learn more
68+
</a>
69+
</AlertDescription>
70+
</Alert>
71+
)}
72+
>
73+
<Switch
74+
checked={enabled}
75+
onCheckedChange={handleToggle}
76+
disabled={isLoading || !isEmailServiceConfigured}
77+
/>
78+
</BasicSettingsCard>
79+
)
80+
}

packages/web/src/app/(app)/settings/security/page.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import { AnonymousAccessEnabledSettingsCard } from "./components/anonymousAccess
22
import { InviteLinkEnabledSettingsCard } from "./components/inviteLinkEnabledSettingsCard";
33
import { MemberApprovalRequiredSettingsCard } from "./components/memberApprovalRequiredSettingsCard";
44
import { CredentialsLoginEnabledSettingsCard } from "./components/credentialsLoginEnabledSettingsCard";
5+
import { EmailCodeLoginEnabledSettingsCard } from "./components/emailCodeLoginEnabledSettingsCard";
56
import { isAnonymousAccessEnabled } from "@/lib/entitlements";
67
import { createInviteLink } from "@/lib/utils";
78
import { authenticatedPage } from "@/middleware/authenticatedPage";
89
import { OrgRole } from "@sourcebot/db";
9-
import { env, isCredentialsLoginEnabled, isMemberApprovalRequired } from "@sourcebot/shared";
10+
import { env, getSMTPConnectionURL, isCredentialsLoginEnabled, isEmailCodeLoginEnabled, isMemberApprovalRequired } from "@sourcebot/shared";
1011
import { SettingsCardGroup } from "../components/settingsCard";
1112

1213
export default authenticatedPage(async ({ org }) => {
@@ -50,6 +51,10 @@ export default authenticatedPage(async ({ org }) => {
5051
<CredentialsLoginEnabledSettingsCard
5152
isCredentialsLoginEnabled={isCredentialsLoginEnabled(org)}
5253
/>
54+
<EmailCodeLoginEnabledSettingsCard
55+
isEmailCodeLoginEnabled={isEmailCodeLoginEnabled(org)}
56+
isEmailServiceConfigured={!!getSMTPConnectionURL() && !!env.EMAIL_FROM_ADDRESS}
57+
/>
5358
</SettingsCardGroup>
5459
</div>
5560
</div>

packages/web/src/lib/errorCodes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export enum ErrorCode {
4040
LIGHTHOUSE_UNREACHABLE = 'LIGHTHOUSE_UNREACHABLE',
4141
EMAIL_LOGIN_CONTROLLED_BY_ENV = 'EMAIL_LOGIN_CONTROLLED_BY_ENV',
4242
EMAIL_LOGIN_CANNOT_BE_DISABLED = 'EMAIL_LOGIN_CANNOT_BE_DISABLED',
43+
EMAIL_CODE_LOGIN_CONTROLLED_BY_ENV = 'EMAIL_CODE_LOGIN_CONTROLLED_BY_ENV',
44+
EMAIL_CODE_LOGIN_CANNOT_BE_DISABLED = 'EMAIL_CODE_LOGIN_CANNOT_BE_DISABLED',
4345
MEMBER_APPROVAL_CONTROLLED_BY_ENV = 'MEMBER_APPROVAL_CONTROLLED_BY_ENV',
4446
ANONYMOUS_ACCESS_CONTROLLED_BY_ENV = 'ANONYMOUS_ACCESS_CONTROLLED_BY_ENV',
4547
}

0 commit comments

Comments
 (0)