diff --git a/.env.example b/.env.example index 531c8dcd..318077f1 100644 --- a/.env.example +++ b/.env.example @@ -45,17 +45,10 @@ PASSWORD_LOGIN_ENABLED=false # local: NEXT_PUBLIC_AUTH_ALLOWED_CALLBACK_ORIGINS=http://app1.local.example.com:3001 # NEXT_PUBLIC_AUTH_ALLOWED_CALLBACK_ORIGINS= -# Optional Resend-backed email login and password reset emails +# Optional Resend-backed password reset emails RESEND_API_KEY= RESEND_FROM="Knowhere " -# Optional OAuth providers -GITHUB_CLIENT_ID= -GITHUB_CLIENT_SECRET= -GOOGLE_CLIENT_ID= -GOOGLE_CLIENT_SECRET= -APPLE_CLIENT_ID= - # Optional analytics NEXT_PUBLIC_POSTHOG_KEY= NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com diff --git a/app/(auth)/_components/oauth-buttons.tsx b/app/(auth)/_components/oauth-buttons.tsx deleted file mode 100644 index ecfbdce4..00000000 --- a/app/(auth)/_components/oauth-buttons.tsx +++ /dev/null @@ -1,112 +0,0 @@ -"use client"; - -import { Button } from "@components/ui/button"; -import { buildPostHogAuthCallbackURL, markPendingAuthLogin } from "@lib/posthog"; -import { Github, Loader2 } from "lucide-react"; -import { useSearchParams } from "next/navigation"; -import { useTranslations } from "next-intl"; -import { useState } from "react"; -import { useToast } from "@/hooks/use-toast"; -import { authRedirect } from "@/lib/auth-redirect"; -import { authClient } from "@/lib/better-auth-client"; - -type OAuthButtonsProps = { - onError?: (error: string) => void; -}; - -export function OAuthButtons({ onError }: OAuthButtonsProps) { - const toast = useToast(); - const [isLoading, setIsLoading] = useState(false); - const [clickedProvider, setClickedProvider] = useState<"google" | "github" | null>(null); - const searchParams = useSearchParams(); - const t = useTranslations("Auth"); - const rawCallbackURL = searchParams.get("callbackURL"); - const callbackURL = authRedirect.resolveCallbackURL(rawCallbackURL); - const errorCallbackURL = authRedirect.buildAuthPagePath("/login", { - callbackURL: rawCallbackURL, - error: "oauth", - }); - - const signInWithProvider = async (provider: "github" | "google") => { - if (isLoading) return; - setIsLoading(true); - setClickedProvider(provider); - try { - const trackedCallbackURL = buildPostHogAuthCallbackURL(callbackURL); - markPendingAuthLogin(); - await authClient.signIn.social({ - provider, - callbackURL: trackedCallbackURL, - errorCallbackURL, - newUserCallbackURL: trackedCallbackURL, - }); - } catch (error) { - const message = error instanceof Error ? error.message : t("loginFailed"); - toast.error(t("oauthFailed"), message); - onError?.(message); - setIsLoading(false); - setClickedProvider(null); - } - }; - - return ( -
- - - - -
-
- -
-
- - {t("orContinueWithEmail")} - -
-
-
- ); -} diff --git a/app/(auth)/callback/apple/page.tsx b/app/(auth)/callback/apple/page.tsx deleted file mode 100644 index 056ff02f..00000000 --- a/app/(auth)/callback/apple/page.tsx +++ /dev/null @@ -1,69 +0,0 @@ -"use client"; - -import { isAuthEventTracked, isLikelyNewUser, trackLogin, trackSignUp } from "@lib/posthog"; -import { useRouter, useSearchParams } from "next/navigation"; -import { useTranslations } from "next-intl"; -import { Suspense, useEffect, useRef } from "react"; -import { useToast } from "@/hooks/use-toast"; -import { authRedirect } from "@/lib/auth-redirect"; -import { authClient } from "@/lib/better-auth-client"; - -function AppleCallbackContent() { - const router = useRouter(); - const searchParams = useSearchParams(); - const toast = useToast(); - const session = authClient.useSession(); - const t = useTranslations("Auth"); - const rawCallbackURL = searchParams.get("callbackURL"); - const callbackURL = authRedirect.resolveCallbackURL(rawCallbackURL); - const loginPath = authRedirect.buildAuthPagePath("/login", { - callbackURL: rawCallbackURL, - error: "oauth", - }); - const hasTrackedLogin = useRef(false); - - useEffect(() => { - if (session.isPending) return; - if (session.data?.user) { - if (!hasTrackedLogin.current && !isAuthEventTracked()) { - if (isLikelyNewUser(session.data.user.createdAt)) { - trackSignUp("apple", session.data.user.id); - } else { - trackLogin("apple", session.data.user.id); - } - hasTrackedLogin.current = true; - } - toast.success(t("appleLoginSuccess")); - router.replace(callbackURL); - } else { - const error = searchParams.get("error"); - if (error) { - toast.error(t("appleLoginFailed")); - router.replace(loginPath); - } - } - }, [callbackURL, loginPath, session.isPending, session.data, toast, router, t, searchParams]); - - return ( -
-
-
-

{t("processingAppleLogin")}

-
-
- ); -} - -export default function AppleCallbackPage() { - return ( - -
-
- } - > - -
- ); -} diff --git a/app/(auth)/callback/github/page.tsx b/app/(auth)/callback/github/page.tsx deleted file mode 100644 index c2112044..00000000 --- a/app/(auth)/callback/github/page.tsx +++ /dev/null @@ -1,52 +0,0 @@ -"use client"; - -import { isAuthEventTracked, isLikelyNewUser, trackLogin, trackSignUp } from "@lib/posthog"; -import { useRouter, useSearchParams } from "next/navigation"; -import { useTranslations } from "next-intl"; -import { useEffect, useRef } from "react"; -import { useToast } from "@/hooks/use-toast"; -import { authRedirect } from "@/lib/auth-redirect"; -import { authClient } from "@/lib/better-auth-client"; - -export default function GitHubCallbackPage() { - const router = useRouter(); - const searchParams = useSearchParams(); - const toast = useToast(); - const session = authClient.useSession(); - const t = useTranslations("Auth"); - const rawCallbackURL = searchParams.get("callbackURL"); - const callbackURL = authRedirect.resolveCallbackURL(rawCallbackURL); - const loginPath = authRedirect.buildAuthPagePath("/login", { - callbackURL: rawCallbackURL, - error: "oauth", - }); - const hasTrackedLogin = useRef(false); - - useEffect(() => { - if (session.isPending) return; - if (session.data?.user) { - if (!hasTrackedLogin.current && !isAuthEventTracked()) { - if (isLikelyNewUser(session.data.user.createdAt)) { - trackSignUp("github", session.data.user.id); - } else { - trackLogin("github", session.data.user.id); - } - hasTrackedLogin.current = true; - } - toast.success(t("githubLoginSuccess")); - router.replace(callbackURL); - } else { - toast.error(t("githubLoginFailed")); - router.replace(loginPath); - } - }, [callbackURL, loginPath, session.isPending, session.data, toast, router, t]); - - return ( -
-
-
-

{t("processingGithubLogin")}

-
-
- ); -} diff --git a/app/(auth)/callback/magic-link/page.tsx b/app/(auth)/callback/magic-link/page.tsx deleted file mode 100644 index 726194f8..00000000 --- a/app/(auth)/callback/magic-link/page.tsx +++ /dev/null @@ -1,53 +0,0 @@ -"use client"; - -import { isAuthEventTracked, isLikelyNewUser, trackLogin, trackSignUp } from "@lib/posthog"; -import { useRouter, useSearchParams } from "next/navigation"; -import { useTranslations } from "next-intl"; -import { useEffect, useRef } from "react"; -import { useToast } from "@/hooks/use-toast"; -import { authRedirect } from "@/lib/auth-redirect"; -import { authClient } from "@/lib/better-auth-client"; - -export default function MagicLinkCallbackPage() { - const router = useRouter(); - const searchParams = useSearchParams(); - const toast = useToast(); - const session = authClient.useSession(); - const t = useTranslations("Auth"); - const rawCallbackURL = searchParams.get("callbackURL"); - const callbackURL = authRedirect.resolveCallbackURL(rawCallbackURL); - const loginPath = authRedirect.buildAuthPagePath("/login", { - callbackURL: rawCallbackURL, - error: "magic", - }); - const hasTrackedLogin = useRef(false); - - useEffect(() => { - if (session.isPending) return; - - if (session.data?.user) { - if (!hasTrackedLogin.current && !isAuthEventTracked()) { - if (isLikelyNewUser(session.data.user.createdAt)) { - trackSignUp("email", session.data.user.id); - } else { - trackLogin("email", session.data.user.id); - } - hasTrackedLogin.current = true; - } - toast.success(t("magicLinkLoginSuccess")); - router.replace(callbackURL); - } else { - toast.error(t("magicLinkLoginFailed")); - router.replace(loginPath); - } - }, [callbackURL, loginPath, session.isPending, session.data, toast, router, t]); - - return ( -
-
-
-

{t("processingMagicLink")}

-
-
- ); -} diff --git a/app/(auth)/login/_components/email-login-form.tsx b/app/(auth)/login/_components/email-login-form.tsx deleted file mode 100644 index 138d240d..00000000 --- a/app/(auth)/login/_components/email-login-form.tsx +++ /dev/null @@ -1,178 +0,0 @@ -"use client"; - -import { Input } from "@components/ui/input"; -import { Label } from "@components/ui/label"; -import { zodResolver } from "@hookform/resolvers/zod"; -import Link from "next/link"; -import { useTranslations } from "next-intl"; -import { useMemo, useState } from "react"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; -import { LoginButton } from "@/app/(auth)/login/_components/login-button"; - -type EmailLoginFormProps = { - disabled?: boolean; - forgotPasswordPath: string; - isMagicLinkLoading?: boolean; - isPasswordLoading?: boolean; - onMagicLinkSubmit: (email: string) => Promise; - onPasswordSubmit: (email: string, password: string) => Promise; - passwordLoginEnabled: boolean; - registerPath: string; -}; - -type LoginFormValues = { - email: string; - password: string; -}; - -export const EmailLoginForm = ({ - disabled = false, - forgotPasswordPath, - isMagicLinkLoading = false, - isPasswordLoading = false, - onMagicLinkSubmit, - onPasswordSubmit, - passwordLoginEnabled, - registerPath, -}: EmailLoginFormProps) => { - const t = useTranslations("Auth"); - const [isPasswordLoginEnabled, setIsPasswordLoginEnabled] = useState(false); - const isSubmitting = isMagicLinkLoading || isPasswordLoading; - - const loginSchema = useMemo( - () => - z.object({ - email: z.string().trim().email(t("emailInvalid")), - password: z.string(), - }), - [t] - ); - - const form = useForm({ - defaultValues: { - email: "", - password: "", - }, - resolver: zodResolver(loginSchema), - }); - - const togglePasswordLogin = () => { - setIsPasswordLoginEnabled((currentValue) => !currentValue); - form.clearErrors("password"); - }; - - const handleSubmit = form.handleSubmit(async ({ email, password }) => { - if (passwordLoginEnabled && isPasswordLoginEnabled) { - if (password.length < 8) { - form.setError("password", { - type: "manual", - message: t("passwordMinLength"), - }); - return; - } - - await onPasswordSubmit(email, password); - return; - } - - await onMagicLinkSubmit(email); - }); - - return ( -
-
- - - {form.formState.errors.email ? ( -

- {form.formState.errors.email.message} -

- ) : null} -
- - {passwordLoginEnabled && isPasswordLoginEnabled ? ( -
-
- - - {t("forgotPassword")} - -
- - {form.formState.errors.password ? ( -

- {form.formState.errors.password.message} -

- ) : null} -
- ) : null} - - - {passwordLoginEnabled && isPasswordLoginEnabled - ? t("signInWithPassword") - : isMagicLinkLoading - ? t("sending") - : t("sendMagicLink")} - - - {passwordLoginEnabled ? ( - <> - - {isPasswordLoginEnabled ? t("useEmailLinkInstead") : t("loginWithPassword")} - - -

- {t("noAccount")}{" "} - - {t("signUpWithPassword")} - -

- - ) : null} -
- ); -}; diff --git a/app/(auth)/login/_components/login-page-shell.tsx b/app/(auth)/login/_components/login-page-shell.tsx index ba81f567..4bb36b84 100644 --- a/app/(auth)/login/_components/login-page-shell.tsx +++ b/app/(auth)/login/_components/login-page-shell.tsx @@ -2,26 +2,14 @@ import Link from "next/link"; import { useTranslations } from "next-intl"; -import { EmailLoginForm } from "@/app/(auth)/login/_components/email-login-form"; import { LoginBrand } from "@/app/(auth)/login/_components/login-brand"; -import { SocialLoginButtons } from "@/app/(auth)/login/_components/social-login-buttons"; +import { UsernameLoginForm } from "@/app/(auth)/login/_components/username-login-form"; import { useLoginActions } from "@/app/(auth)/login/_hooks/use-login-actions"; -import { useAppConfigContext } from "@/providers/config-provider"; export const LoginPageShell = () => { const t = useTranslations("Auth"); - const { passwordLoginEnabled } = useAppConfigContext(); - const { - activeOAuthProvider, - forgotPasswordPath, - registerPath, - isMagicLinkLoading, - isOAuthLoading, - isPasswordLoading, - signInWithMagicLink, - signInWithPassword, - signInWithProvider, - } = useLoginActions(); + const { forgotPasswordPath, registerPath, isPasswordLoading, signInWithPassword } = + useLoginActions(); return (
@@ -46,30 +34,12 @@ export const LoginPageShell = () => {

{t("login")}

-
- - -
diff --git a/app/(auth)/login/_components/social-login-buttons.tsx b/app/(auth)/login/_components/social-login-buttons.tsx deleted file mode 100644 index 83a124d1..00000000 --- a/app/(auth)/login/_components/social-login-buttons.tsx +++ /dev/null @@ -1,78 +0,0 @@ -"use client"; - -import { Loader2 } from "lucide-react"; -import { useTranslations } from "next-intl"; -import { LoginButton } from "@/app/(auth)/login/_components/login-button"; -import type { OAuthProvider } from "@/app/(auth)/login/_hooks/use-login-actions"; - -type SocialLoginButtonsProps = { - activeProvider: OAuthProvider | null; - disabled?: boolean; - onSignIn: (provider: OAuthProvider) => Promise; -}; - -const GoogleIcon = () => ( - -); - -const GitHubIcon = () => ( - -); - -export const SocialLoginButtons = ({ - activeProvider, - disabled = false, - onSignIn, -}: SocialLoginButtonsProps) => { - const t = useTranslations("Auth"); - - return ( -
- : - } - onClick={() => onSignIn("google")} - variant="secondary" - > - {t("continueWithGoogle")} - - - : - } - onClick={() => onSignIn("github")} - variant="secondary" - > - {t("continueWithGithub")} - -
- ); -}; diff --git a/app/(auth)/login/_components/username-login-form.tsx b/app/(auth)/login/_components/username-login-form.tsx new file mode 100644 index 00000000..8feed8ec --- /dev/null +++ b/app/(auth)/login/_components/username-login-form.tsx @@ -0,0 +1,143 @@ +"use client"; + +import { Input } from "@components/ui/input"; +import { Label } from "@components/ui/label"; +import { zodResolver } from "@hookform/resolvers/zod"; +import Link from "next/link"; +import { useTranslations } from "next-intl"; +import { useMemo } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { LoginButton } from "@/app/(auth)/login/_components/login-button"; + +type UsernameLoginFormProps = { + disabled?: boolean; + forgotPasswordPath: string; + isPasswordLoading?: boolean; + onPasswordSubmit: (username: string, password: string) => Promise; + registerPath: string; +}; + +type LoginFormValues = { + username: string; + password: string; +}; + +export const UsernameLoginForm = ({ + disabled = false, + forgotPasswordPath, + isPasswordLoading = false, + onPasswordSubmit, + registerPath, +}: UsernameLoginFormProps) => { + const t = useTranslations("Auth"); + const isSubmitting = isPasswordLoading; + + const loginSchema = useMemo( + () => + z.object({ + username: z.string().trim().min(2, t("usernameMinLength")), + password: z.string(), + }), + [t] + ); + + const form = useForm({ + defaultValues: { + username: "", + password: "", + }, + resolver: zodResolver(loginSchema), + }); + + const handleSubmit = form.handleSubmit(async ({ username, password }) => { + if (password.length < 8) { + form.setError("password", { + type: "manual", + message: t("passwordMinLength"), + }); + return; + } + + await onPasswordSubmit(username, password); + }); + + return ( +
+
+ + + {form.formState.errors.username ? ( +

+ {form.formState.errors.username.message} +

+ ) : null} +
+ +
+
+ + + {t("forgotPassword")} + +
+ + {form.formState.errors.password ? ( +

+ {form.formState.errors.password.message} +

+ ) : null} +
+ + + {t("signInWithPassword")} + + +

+ {t("noAccount")}{" "} + + {t("signUpWithPassword")} + +

+
+ ); +}; diff --git a/app/(auth)/login/_hooks/use-login-actions.ts b/app/(auth)/login/_hooks/use-login-actions.ts index d6d41d48..6fb50d5f 100644 --- a/app/(auth)/login/_hooks/use-login-actions.ts +++ b/app/(auth)/login/_hooks/use-login-actions.ts @@ -1,11 +1,6 @@ "use client"; -import { - buildPostHogAuthCallbackURL, - markPendingAuthLogin, - markPendingMagicLinkAuth, - trackLogin, -} from "@lib/posthog"; +import { trackLogin } from "@lib/posthog"; import { useRouter, useSearchParams } from "next/navigation"; import { useTranslations } from "next-intl"; import { useState } from "react"; @@ -13,27 +8,15 @@ import { useToast } from "@/hooks/use-toast"; import { authRedirect } from "@/lib/auth-redirect"; import { authClient } from "@/lib/better-auth-client"; -export type OAuthProvider = "github" | "google"; - export const useLoginActions = () => { const searchParams = useSearchParams(); const router = useRouter(); const t = useTranslations("Auth"); const toast = useToast(); - const [isMagicLinkLoading, setIsMagicLinkLoading] = useState(false); const [isPasswordLoading, setIsPasswordLoading] = useState(false); - const [activeOAuthProvider, setActiveOAuthProvider] = useState(null); const rawCallbackURL = searchParams.get("callbackURL"); const callbackURL = authRedirect.resolveCallbackURL(rawCallbackURL); - const oauthErrorCallbackURL = authRedirect.buildAuthPagePath("/login", { - callbackURL: rawCallbackURL, - error: "oauth", - }); - const magicLinkErrorCallbackURL = authRedirect.buildMagicLinkErrorCallbackURL("/login", { - callbackURL: rawCallbackURL, - error: "magic", - }); const forgotPasswordPath = authRedirect.buildAuthPagePath("/forgot-password", { callbackURL: rawCallbackURL, }); @@ -45,71 +28,16 @@ export const useLoginActions = () => { callbackURL: rawCallbackURL, }); - const signInWithProvider = async (provider: OAuthProvider) => { - if (isMagicLinkLoading || isPasswordLoading || activeOAuthProvider) { - return; - } - - setActiveOAuthProvider(provider); - - try { - const trackedCallbackURL = buildPostHogAuthCallbackURL(callbackURL); - markPendingAuthLogin(); - await authClient.signIn.social({ - provider, - callbackURL: trackedCallbackURL, - errorCallbackURL: oauthErrorCallbackURL, - newUserCallbackURL: trackedCallbackURL, - }); - } catch (error) { - const message = error instanceof Error ? error.message : t("loginFailed"); - toast.error(t("oauthFailed"), message); - setActiveOAuthProvider(null); - } - }; - - const signInWithMagicLink = async (email: string) => { - if (isMagicLinkLoading || isPasswordLoading || activeOAuthProvider) { - return false; - } - - setIsMagicLinkLoading(true); - - try { - const trackedCallbackURL = buildPostHogAuthCallbackURL(callbackURL, "magic"); - markPendingMagicLinkAuth(); - const { error } = await authClient.signIn.magicLink({ - email: email.trim(), - callbackURL: trackedCallbackURL, - errorCallbackURL: magicLinkErrorCallbackURL, - newUserCallbackURL: trackedCallbackURL, - }); - - if (error) { - throw new Error(error.message || t("magicLinkFailed")); - } - - toast.success(t("magicLinkSent")); - return true; - } catch (error) { - const message = error instanceof Error ? error.message : t("loginFailed"); - toast.error(t("loginFailed"), message); - return false; - } finally { - setIsMagicLinkLoading(false); - } - }; - - const signInWithPassword = async (email: string, password: string) => { - if (isMagicLinkLoading || isPasswordLoading || activeOAuthProvider) { + const signInWithPassword = async (username: string, password: string) => { + if (isPasswordLoading) { return false; } setIsPasswordLoading(true); try { - const { error } = await authClient.signIn.email({ - email: email.trim(), + const { error } = await authClient.signIn.username({ + username: username.trim(), password, }); @@ -136,14 +64,9 @@ export const useLoginActions = () => { }; return { - activeOAuthProvider, forgotPasswordPath, registerPath, - isMagicLinkLoading, - isOAuthLoading: activeOAuthProvider !== null, isPasswordLoading, - signInWithMagicLink, signInWithPassword, - signInWithProvider, }; }; diff --git a/app/(auth)/register/page.tsx b/app/(auth)/register/page.tsx index 5e8c6746..0feafa76 100644 --- a/app/(auth)/register/page.tsx +++ b/app/(auth)/register/page.tsx @@ -12,7 +12,6 @@ import { useTranslations } from "next-intl"; import { useMemo, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; -import { OAuthButtons } from "@/app/(auth)/_components/oauth-buttons"; import { useToast } from "@/hooks/use-toast"; import { authRedirect } from "@/lib/auth-redirect"; import { authClient } from "@/lib/better-auth-client"; @@ -60,6 +59,7 @@ export default function RegisterPage() { try { const { error } = await authClient.signUp.email({ name: data.username, + username: data.username, email: data.email, callbackURL, password: data.password, @@ -85,10 +85,6 @@ export default function RegisterPage() { } }; - const handleOAuthError = (error: string) => { - toast.error(t("oauthFailed"), error); - }; - return ( @@ -96,8 +92,6 @@ export default function RegisterPage() { {t("registerDesc")} - -
@@ -105,7 +99,7 @@ export default function RegisterPage() { id="username" type="text" placeholder={t("usernamePlaceholder")} - autoComplete="name" + autoComplete="username" {...register("username")} disabled={isLoading} /> diff --git a/app/(dashboard)/settings/_components/settings-page.tsx b/app/(dashboard)/settings/_components/settings-page.tsx index 0d3cac49..c10e4169 100644 --- a/app/(dashboard)/settings/_components/settings-page.tsx +++ b/app/(dashboard)/settings/_components/settings-page.tsx @@ -72,10 +72,10 @@ const infoFieldLabelClassName = "text-xs leading-[14px] text-[#9f9fa9] lg:leadin const fieldValueClassName = "text-xs leading-[18px] text-[#18181b] dark:text-[#e4e4e7] lg:text-sm lg:leading-5"; -const createProfileSchema = (messages: { emailInvalid: string; usernameMinLength: string }) => +const createProfileSchema = (messages: { emailInvalid: string; displayNameMinLength: string }) => z.object({ email: z.string().email({ message: messages.emailInvalid }), - username: z.string().min(2, { message: messages.usernameMinLength }), + displayName: z.string().min(2, { message: messages.displayNameMinLength }), }); type ProfileFormValues = z.infer>; @@ -172,12 +172,12 @@ export const SettingsPage = () => { const profileForm = useForm({ defaultValues: { email: "", - username: "", + displayName: "", }, resolver: zodResolver( createProfileSchema({ emailInvalid: t("emailInvalid"), - usernameMinLength: t("usernameMinLength"), + displayNameMinLength: t("displayNameMinLength"), }) ), }); @@ -227,7 +227,7 @@ export const SettingsPage = () => { profileForm.reset({ email: userEmail, - username: userName, + displayName: userName, }); }, [hasUser, profileForm, userEmail, userName]); @@ -293,16 +293,16 @@ export const SettingsPage = () => { } try { - const usernameChanged = values.username !== user.name; + const displayNameChanged = values.displayName !== user.name; const emailChanged = values.email !== user.email; - if (!usernameChanged && !emailChanged) { + if (!displayNameChanged && !emailChanged) { toast.error(t("noChanges")); return; } - if (usernameChanged) { - await updateProfileMutation.mutateAsync({ name: values.username }); + if (displayNameChanged) { + await updateProfileMutation.mutateAsync({ name: values.displayName }); } if (emailChanged && !hasOAuthAccount) { @@ -468,7 +468,7 @@ export const SettingsPage = () => { unverifiedLabel={t("unverified")} user={user} userIdLabel={t("userId")} - usernameLabel={t("username")} + displayNameLabel={t("displayName")} verifiedLabel={t("verified")} /> @@ -601,7 +601,7 @@ const SettingsProfileSection = ({ unverifiedLabel, user, userIdLabel, - usernameLabel, + displayNameLabel, verifiedLabel, }: { accountInformationLabel: string; @@ -630,7 +630,7 @@ const SettingsProfileSection = ({ unverifiedLabel: string; user: AuthUser; userIdLabel: string; - usernameLabel: string; + displayNameLabel: string; verifiedLabel: string; }) => { return ( @@ -645,17 +645,17 @@ const SettingsProfileSection = ({ >
- {form.formState.errors.username ? ( + {form.formState.errors.displayName ? (

- {form.formState.errors.username.message} + {form.formState.errors.displayName.message}

) : null}
diff --git a/drizzle/0010_wet_omega_red.sql b/drizzle/0010_wet_omega_red.sql new file mode 100644 index 00000000..9e94dfe3 --- /dev/null +++ b/drizzle/0010_wet_omega_red.sql @@ -0,0 +1,3 @@ +ALTER TABLE "user" ADD COLUMN "username" text;--> statement-breakpoint +ALTER TABLE "user" ADD COLUMN "displayUsername" text;--> statement-breakpoint +ALTER TABLE "user" ADD CONSTRAINT "user_username_unique" UNIQUE("username"); \ No newline at end of file diff --git a/drizzle/meta/0010_snapshot.json b/drizzle/meta/0010_snapshot.json new file mode 100644 index 00000000..129d8858 --- /dev/null +++ b/drizzle/meta/0010_snapshot.json @@ -0,0 +1,1115 @@ +{ + "id": "898ffca9-5642-46cc-a291-03f35509177c", + "prevId": "918af8cf-d54a-4b54-a5de-5cc326733c10", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "accountId": { + "name": "accountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerId": { + "name": "providerId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idToken": { + "name": "idToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessTokenExpiresAt": { + "name": "accessTokenExpiresAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refreshTokenExpiresAt": { + "name": "refreshTokenExpiresAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.emailVerificationToken": { + "name": "emailVerificationToken", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "emailVerificationToken_userId_user_id_fk": { + "name": "emailVerificationToken_userId_user_id_fk", + "tableFrom": "emailVerificationToken", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "emailVerificationToken_token_unique": { + "name": "emailVerificationToken_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.jwks": { + "name": "jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "publicKey": { + "name": "publicKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "privateKey": { + "name": "privateKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ipAddress": { + "name": "ipAddress", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userAgent": { + "name": "userAgent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "displayUsername": { + "name": "displayUsername", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "usageWelcomeStatus": { + "name": "usageWelcomeStatus", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "usageWelcomeApiKey": { + "name": "usageWelcomeApiKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_username_unique": { + "name": "user_username_unique", + "nullsNotDistinct": false, + "columns": ["username"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.marketing_attribution_sessions": { + "name": "marketing_attribution_sessions", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "utm_source": { + "name": "utm_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_medium": { + "name": "utm_medium", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_campaign": { + "name": "utm_campaign", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_content": { + "name": "utm_content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_term": { + "name": "utm_term", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oppref": { + "name": "oppref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "landing_path": { + "name": "landing_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "referrer_host": { + "name": "referrer_host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "captured_at": { + "name": "captured_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "bound_user_id": { + "name": "bound_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bound_at": { + "name": "bound_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "marketingAttributionSession_boundUserId_idx": { + "name": "marketingAttributionSession_boundUserId_idx", + "columns": [ + { + "expression": "bound_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "marketingAttributionSession_capturedAt_idx": { + "name": "marketingAttributionSession_capturedAt_idx", + "columns": [ + { + "expression": "captured_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "marketingAttributionSession_sourceCampaign_idx": { + "name": "marketingAttributionSession_sourceCampaign_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "utm_campaign", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "marketing_attribution_sessions_bound_user_id_user_id_fk": { + "name": "marketing_attribution_sessions_bound_user_id_user_id_fk", + "tableFrom": "marketing_attribution_sessions", + "tableTo": "user", + "columnsFrom": ["bound_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.marketing_page_views": { + "name": "marketing_page_views", + "schema": "", + "columns": { + "view_id": { + "name": "view_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "acquisition_session_id": { + "name": "acquisition_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "utm_source": { + "name": "utm_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_medium": { + "name": "utm_medium", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_campaign": { + "name": "utm_campaign", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_content": { + "name": "utm_content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_term": { + "name": "utm_term", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oppref": { + "name": "oppref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "visited_path": { + "name": "visited_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "referrer_host": { + "name": "referrer_host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "viewed_at": { + "name": "viewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "marketingPageView_viewedAt_idx": { + "name": "marketingPageView_viewedAt_idx", + "columns": [ + { + "expression": "viewed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "marketingPageView_visitedPath_viewedAt_idx": { + "name": "marketingPageView_visitedPath_viewedAt_idx", + "columns": [ + { + "expression": "visited_path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "viewed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "marketingPageView_acquisitionSessionId_idx": { + "name": "marketingPageView_acquisitionSessionId_idx", + "columns": [ + { + "expression": "acquisition_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.newsletterSubscription": { + "name": "newsletterSubscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "confirmationTokenHash": { + "name": "confirmationTokenHash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmationTokenExpiresAt": { + "name": "confirmationTokenExpiresAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "confirmationSentAt": { + "name": "confirmationSentAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "confirmedAt": { + "name": "confirmedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "unsubscribedAt": { + "name": "unsubscribedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "newsletterSubscription_email_unique": { + "name": "newsletterSubscription_email_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "newsletterSubscription_confirmationTokenHash_unique": { + "name": "newsletterSubscription_confirmationTokenHash_unique", + "columns": [ + { + "expression": "confirmationTokenHash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "newsletterSubscription_status_idx": { + "name": "newsletterSubscription_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauthAuthorizationCode": { + "name": "oauthAuthorizationCode", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "codeHash": { + "name": "codeHash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirectUri": { + "name": "redirectUri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "codeChallenge": { + "name": "codeChallenge", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "clientName": { + "name": "clientName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full_access'" + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumedAt": { + "name": "consumedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oauthAuthorizationCode_codeHash_unique": { + "name": "oauthAuthorizationCode_codeHash_unique", + "columns": [ + { + "expression": "codeHash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauthAuthorizationCode_userId_idx": { + "name": "oauthAuthorizationCode_userId_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauthAuthorizationCode_userId_user_id_fk": { + "name": "oauthAuthorizationCode_userId_user_id_fk", + "tableFrom": "oauthAuthorizationCode", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauthRefreshToken": { + "name": "oauthRefreshToken", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tokenHash": { + "name": "tokenHash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full_access'" + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "revokedAt": { + "name": "revokedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lastUsedAt": { + "name": "lastUsedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oauthRefreshToken_tokenHash_unique": { + "name": "oauthRefreshToken_tokenHash_unique", + "columns": [ + { + "expression": "tokenHash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauthRefreshToken_userId_idx": { + "name": "oauthRefreshToken_userId_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauthRefreshToken_userId_user_id_fk": { + "name": "oauthRefreshToken_userId_user_id_fk", + "tableFrom": "oauthRefreshToken", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 5e055ea3..93708d86 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -71,6 +71,13 @@ "when": 1784879812653, "tag": "0009_mushy_ezekiel", "breakpoints": true + }, + { + "idx": 10, + "version": "7", + "when": 1785569463657, + "tag": "0010_wet_omega_red", + "breakpoints": true } ] } diff --git a/i18n/locales/en.json b/i18n/locales/en.json index c14bf3b6..c3ac309e 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -139,12 +139,9 @@ "confirmPasswordPlaceholder": "Enter your password again", "username": "Username", "usernamePlaceholder": "Enter your username", - "sendMagicLink": "Sign in with Email", "sending": "Sending...", "signingIn": "Signing in...", "signInWithPassword": "Sign in", - "loginWithPassword": "Sign in with password", - "useEmailLinkInstead": "Use email link instead", "signUpWithPassword": "Create account", "registering": "Signing up...", "haveAccount": "Already have an account?", @@ -152,8 +149,6 @@ "loginNow": "Sign in now", "registerNow": "Create one", "emailInvalid": "Please enter a valid email address", - "magicLinkFailed": "Failed to send magic link", - "magicLinkSent": "Magic link sent, please check your email", "forgotPassword": "Forgot password?", "forgotPasswordTitle": "Reset password", "forgotPasswordDesc": "Enter your email and we'll send you a password reset link.", @@ -173,21 +168,11 @@ "backToLogin": "Back to sign in", "loginSuccess": "Signed in successfully", "loginFailed": "Login failed", - "oauthFailed": "OAuth login failed", "usernameMinLength": "Username must be at least 2 characters", "passwordMinLength": "Password must be at least 8 characters", "passwordMismatch": "Passwords do not match", "registerSuccess": "Registration successful", - "registerFailed": "Registration failed", - "continueWithGoogle": "Continue with Google", - "continueWithGithub": "Continue with Github", - "orContinueWithEmail": "OR CONTINUE WITH EMAIL", - "githubLoginSuccess": "GitHub login successful", - "githubLoginFailed": "GitHub login failed", - "processingGithubLogin": "Processing GitHub login...", - "appleLoginSuccess": "Apple login successful", - "appleLoginFailed": "Apple login failed", - "processingAppleLogin": "Processing Apple login..." + "registerFailed": "Registration failed" }, "BuyCredits": { "title": "Buy Knowhere API Credit", @@ -243,10 +228,10 @@ "appearance": "Appearance", "theme": "Theme", "notifications": "Notifications", - "username": "User Name", + "displayName": "Display Name", "email": "Email", "saveChanges": "Save Changes", - "usernameMinLength": "Username must be at least 2 characters", + "displayNameMinLength": "Display name must be at least 2 characters", "emailInvalid": "Please enter a valid email address", "loadProfileFailed": "Failed to load user profile", "profileUpdated": "Profile updated successfully", diff --git a/i18n/locales/zh.json b/i18n/locales/zh.json index ed7526cb..c3e4c7d1 100644 --- a/i18n/locales/zh.json +++ b/i18n/locales/zh.json @@ -157,12 +157,9 @@ "confirmPasswordPlaceholder": "请再次输入密码", "username": "用户名", "usernamePlaceholder": "请输入用户名", - "sendMagicLink": "通过邮箱获取登录链接", "sending": "发送中...", "signingIn": "登录中...", "signInWithPassword": "登录", - "loginWithPassword": "使用密码登录", - "useEmailLinkInstead": "改用邮箱登录链接", "signUpWithPassword": "创建账户", "registering": "注册中...", "haveAccount": "已有账户?", @@ -170,8 +167,6 @@ "loginNow": "立即登录", "registerNow": "立即注册", "emailInvalid": "请输入有效的邮箱地址", - "magicLinkFailed": "魔法链接发送失败", - "magicLinkSent": "魔法链接已发送,请检查邮箱", "forgotPassword": "忘记密码?", "forgotPasswordTitle": "重置密码", "forgotPasswordDesc": "输入邮箱,我们会发送密码重置链接。", @@ -191,21 +186,11 @@ "backToLogin": "返回登录", "loginSuccess": "登录成功", "loginFailed": "登录失败", - "oauthFailed": "OAuth登录失败", "usernameMinLength": "用户名至少需要2个字符", "passwordMinLength": "密码至少需要8个字符", "passwordMismatch": "密码不匹配", "registerSuccess": "注册成功", - "registerFailed": "注册失败", - "continueWithGoogle": "使用 Google 继续", - "continueWithGithub": "使用 GitHub 继续", - "orContinueWithEmail": "或使用邮箱继续", - "githubLoginSuccess": "GitHub登录成功", - "githubLoginFailed": "GitHub登录失败", - "processingGithubLogin": "正在处理GitHub登录...", - "appleLoginSuccess": "Apple登录成功", - "appleLoginFailed": "Apple登录失败", - "processingAppleLogin": "正在处理Apple登录..." + "registerFailed": "注册失败" }, "Billing": { "title": "账单与套餐", @@ -244,10 +229,10 @@ "appearance": "外观", "theme": "主题", "notifications": "通知", - "username": "用户名", + "displayName": "显示名称", "email": "邮箱", "saveChanges": "保存更改", - "usernameMinLength": "用户名至少需要2个字符", + "displayNameMinLength": "显示名称至少需要2个字符", "emailInvalid": "请输入有效的邮箱地址", "loadProfileFailed": "加载用户资料失败", "profileUpdated": "个人资料已更新", diff --git a/lib/auth.ts b/lib/auth.ts index 8f1c1f26..216dea80 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -2,7 +2,7 @@ import "./polyfill"; import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { nextCookies } from "better-auth/next-js"; -import { jwt, magicLink } from "better-auth/plugins"; +import { jwt, username } from "better-auth/plugins"; import { Resend } from "resend"; import { ProxyAgent, setGlobalDispatcher } from "undici"; import { authCookies } from "@/lib/auth-cookie-config"; @@ -192,27 +192,6 @@ export const auth = betterAuth({ }, }, - // Email/password is the self-hosted baseline. OAuth and Magic Link remain optional add-ons. - socialProviders: { - ...(env.GITHUB_CLIENT_ID && env.GITHUB_CLIENT_SECRET - ? { - github: { - clientId: env.GITHUB_CLIENT_ID, - clientSecret: env.GITHUB_CLIENT_SECRET, - redirectURI: `${env.BETTER_AUTH_URL}/api/auth/callback/github`, - }, - } - : {}), - ...(env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET - ? { - google: { - clientId: env.GOOGLE_CLIENT_ID, - clientSecret: env.GOOGLE_CLIENT_SECRET, - redirectURI: `${env.BETTER_AUTH_URL}/api/auth/callback/google`, - }, - } - : {}), - }, plugins: [ jwt({ jwt: { @@ -222,19 +201,8 @@ export const auth = betterAuth({ }), }, }), - magicLink({ - sendMagicLink: async ({ email, url }) => { - await sendAuthEmail({ - to: email, - subject: "Knowhere Account Login Link", - title: "Log in to Knowhere", - intro: "You requested a login link. Click the button below to sign in:", - buttonText: "Sign In", - url, - fallbackLabel: "Magic link", - missingApiKeyMessage: "RESEND_API_KEY is required for magic-link email login", - }); - }, + username({ + minUsernameLength: 2, }), nextCookies(), ], diff --git a/lib/better-auth-client.ts b/lib/better-auth-client.ts index eb7080b1..ee8bde51 100644 --- a/lib/better-auth-client.ts +++ b/lib/better-auth-client.ts @@ -1,10 +1,10 @@ "use client"; -import { magicLinkClient } from "better-auth/client/plugins"; +import { usernameClient } from "better-auth/client/plugins"; import { createAuthClient } from "better-auth/react"; import { env } from "@/lib/env"; -// Shared Better Auth client. Magic Link is optional; email/password is available by default. +// Shared Better Auth client. Email/password is the only sign-in method. export const authClient = createAuthClient({ baseURL: typeof window === "undefined" @@ -12,5 +12,5 @@ export const authClient = createAuthClient({ ? env.NEXT_PUBLIC_AUTH_BASE_URL : `${env.NEXT_PUBLIC_APP_URL}${env.NEXT_PUBLIC_AUTH_BASE_URL}` : `${window.location.origin}/api/auth`, - plugins: [magicLinkClient()], + plugins: [usernameClient()], }); diff --git a/lib/config.ts b/lib/config.ts index 92c2e0a8..995cc634 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -32,10 +32,6 @@ export type AppConfigType = { showIcp: boolean; gaMeasurementId: string; openAIAdsPixelId: string; - // OAuth配置(运行时配置,不带NEXT_PUBLIC_前缀) - googleClientId: string; - githubClientId: string; - appleClientId: string; billingEnabled: boolean; passwordLoginEnabled: boolean; }; @@ -56,11 +52,6 @@ export const getDefaultConfig = (): AppConfigType => { const gaMeasurementId = env.GA_MEASUREMENT_ID ?? ""; const openAIAdsPixelId = env.OPENAI_ADS_PIXEL_ID ?? ""; - // OAuth配置(运行时配置,不带NEXT_PUBLIC_前缀) - const googleClientId = getEnv("GOOGLE_CLIENT_ID", ""); - const githubClientId = getEnv("GITHUB_CLIENT_ID", ""); - const appleClientId = getEnv("APPLE_CLIENT_ID", ""); - return { // 公司名称(运行时配置,不带 NEXT_PUBLIC_ 前缀) companyName, @@ -84,11 +75,6 @@ export const getDefaultConfig = (): AppConfigType => { gaMeasurementId, openAIAdsPixelId, - // OAuth配置(运行时配置) - googleClientId, - githubClientId, - appleClientId, - billingEnabled: isBillingEnabled(), passwordLoginEnabled: getBooleanEnv(env.PASSWORD_LOGIN_ENABLED), }; diff --git a/lib/db/auth-schema.ts b/lib/db/auth-schema.ts index fbc4afc5..9de9b6d0 100644 --- a/lib/db/auth-schema.ts +++ b/lib/db/auth-schema.ts @@ -12,6 +12,8 @@ export const user = pgTable("user", { email: text("email").notNull().unique(), emailVerified: boolean("emailVerified").notNull().default(false), image: text("image"), + username: text("username").unique(), + displayUsername: text("displayUsername"), role: text("role").notNull().default("user"), usageWelcomeStatus: text("usageWelcomeStatus").notNull().default("pending"), usageWelcomeApiKey: text("usageWelcomeApiKey"), @@ -57,7 +59,7 @@ export const account = pgTable("account", { updatedAt: timestamp("updatedAt", { withTimezone: true }).notNull().defaultNow(), }); -// Verification table - stores Magic Link tokens for passwordless login +// Verification table - stores verification tokens (e.g. email verification) export const verification = pgTable("verification", { id: text("id") .primaryKey() diff --git a/lib/env.ts b/lib/env.ts index e5410f4c..14e9765a 100644 --- a/lib/env.ts +++ b/lib/env.ts @@ -28,10 +28,6 @@ export const env = createEnv({ .optional(), OPENAI_ADS_PIXEL_ID: z.preprocess(normalizeOptionalString, z.string().optional()), OPENAI_ADS_CONVERSIONS_API_KEY: z.preprocess(normalizeOptionalString, z.string().optional()), - GITHUB_CLIENT_ID: z.string().optional(), - GITHUB_CLIENT_SECRET: z.string().optional(), - GOOGLE_CLIENT_ID: z.string().optional(), - GOOGLE_CLIENT_SECRET: z.string().optional(), RESEND_API_KEY: z.string().optional(), RESEND_FROM: z.string().default("Knowhere "), BILLING_ENABLED: z.string().default("false"), @@ -95,10 +91,6 @@ export const env = createEnv({ GA_MEASUREMENT_ID: process.env.GA_MEASUREMENT_ID, OPENAI_ADS_PIXEL_ID: process.env.OPENAI_ADS_PIXEL_ID, OPENAI_ADS_CONVERSIONS_API_KEY: process.env.OPENAI_ADS_CONVERSIONS_API_KEY, - GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID, - GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET, - GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, - GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, RESEND_API_KEY: process.env.RESEND_API_KEY, RESEND_FROM: process.env.RESEND_FROM, BILLING_ENABLED: process.env.BILLING_ENABLED, diff --git a/providers/config-provider.tsx b/providers/config-provider.tsx index dbff89a8..5c824b5f 100644 --- a/providers/config-provider.tsx +++ b/providers/config-provider.tsx @@ -27,9 +27,6 @@ export function useAppConfigContext(): AppConfigType { showIcp: false, gaMeasurementId: "", openAIAdsPixelId: "", - googleClientId: "", - githubClientId: "", - appleClientId: "", billingEnabled: false, passwordLoginEnabled: false, };