diff --git a/apps/renderer/src/modules/profile/profile-setting-form.tsx b/apps/renderer/src/modules/profile/profile-setting-form.tsx
index a78e7b0ee2..9ee4a65e97 100644
--- a/apps/renderer/src/modules/profile/profile-setting-form.tsx
+++ b/apps/renderer/src/modules/profile/profile-setting-form.tsx
@@ -10,6 +10,7 @@ import {
FormMessage,
} from "@follow/components/ui/form/index.jsx"
import { Input } from "@follow/components/ui/input/index.js"
+import { Label } from "@follow/components/ui/label/index.js"
import { updateUser } from "@follow/shared/auth"
import { cn } from "@follow/utils/utils"
import { zodResolver } from "@hookform/resolvers/zod"
@@ -74,6 +75,10 @@ export const ProfileSettingForm = ({
return (
+ )
+}
+
+export const UpdatePasswordForm = () => {
+ const { data: hasPassword, isLoading } = useHasPassword()
+
+ if (isLoading || !hasPassword) {
+ return null
+ }
+
+ return
+}
diff --git a/apps/renderer/src/pages/settings/(settings)/profile.tsx b/apps/renderer/src/pages/settings/(settings)/profile.tsx
index 510dfac7ca..0a3464f9dc 100644
--- a/apps/renderer/src/pages/settings/(settings)/profile.tsx
+++ b/apps/renderer/src/pages/settings/(settings)/profile.tsx
@@ -1,4 +1,5 @@
import { ProfileSettingForm } from "~/modules/profile/profile-setting-form"
+import { UpdatePasswordForm } from "~/modules/profile/update-password-form"
import { SettingsTitle } from "~/modules/settings/title"
import { defineSettingPageData } from "~/modules/settings/utils"
@@ -15,6 +16,7 @@ export function Component() {
<>
+
>
)
}
diff --git a/apps/renderer/src/queries/auth.ts b/apps/renderer/src/queries/auth.ts
index 354d9b4199..16751ddba3 100644
--- a/apps/renderer/src/queries/auth.ts
+++ b/apps/renderer/src/queries/auth.ts
@@ -1,4 +1,4 @@
-import { getSession } from "@follow/shared/auth"
+import { getSession, listAccounts } from "@follow/shared/auth"
import type { AuthSession } from "@follow/shared/hono"
import type { FetchError } from "ofetch"
@@ -7,6 +7,23 @@ import { defineQuery } from "~/lib/defineQuery"
export const auth = {
getSession: () => defineQuery(["auth", "session"], () => getSession()),
+ getAccounts: () =>
+ defineQuery(["auth", "accounts"], async () => {
+ const accounts = await listAccounts()
+ return accounts.data as Array<{ id: string; provider: string }>
+ }),
+}
+
+export const useAccounts = () => {
+ return useAuthQuery(auth.getAccounts())
+}
+
+export const useHasPassword = () => {
+ const accounts = useAccounts()
+ return {
+ ...accounts,
+ data: !!accounts.data?.find((account) => account.provider === "credential"),
+ }
}
export const useSession = (options?: { enabled?: boolean }) => {
diff --git a/apps/server/client/modules/login/index.tsx b/apps/server/client/modules/login/index.tsx
new file mode 100644
index 0000000000..949408ae08
--- /dev/null
+++ b/apps/server/client/modules/login/index.tsx
@@ -0,0 +1,302 @@
+import { UserAvatar } from "@client/components/ui/user-avatar"
+import { queryClient } from "@client/lib/query-client"
+import { useSession } from "@client/query/auth"
+import { useAuthProviders } from "@client/query/users"
+import { Logo } from "@follow/components/icons/logo.jsx"
+import { AutoResizeHeight } from "@follow/components/ui/auto-resize-height/index.jsx"
+import { Button, MotionButtonBase } from "@follow/components/ui/button/index.js"
+import { Divider } from "@follow/components/ui/divider/index.js"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@follow/components/ui/form/index.jsx"
+import { Input } from "@follow/components/ui/input/index.js"
+import { LoadingCircle } from "@follow/components/ui/loading/index.jsx"
+import { authProvidersConfig } from "@follow/constants"
+import { createSession, loginHandler, signOut } from "@follow/shared/auth"
+import { DEEPLINK_SCHEME } from "@follow/shared/constants"
+import { cn } from "@follow/utils/utils"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { useCallback, useEffect, useMemo, useRef, useState } from "react"
+import { useForm } from "react-hook-form"
+import { Trans, useTranslation } from "react-i18next"
+import { Link, useLocation, useNavigate } from "react-router"
+import { toast } from "sonner"
+import { z } from "zod"
+
+const overrideProviderIconMap: Record = {
+ apple: ,
+ github: ,
+}
+
+export function Login() {
+ const { status, refetch } = useSession()
+
+ const [redirecting, setRedirecting] = useState(false)
+
+ const { data: authProviders, isLoading } = useAuthProviders()
+
+ const location = useLocation()
+ const urlParams = new URLSearchParams(location.search)
+ const provider = urlParams.get("provider")
+ const isCredentialProvider = provider === "credential"
+
+ const isAuthenticated = status === "authenticated"
+
+ const { t } = useTranslation("external")
+
+ useEffect(() => {
+ if (provider && !isCredentialProvider && status === "unauthenticated") {
+ loginHandler(provider)
+ setRedirecting(true)
+ }
+ }, [isCredentialProvider, provider, status])
+
+ const getCallbackUrl = useCallback(async () => {
+ const { data } = await createSession()
+ if (!data) return null
+ return {
+ url: `${DEEPLINK_SCHEME}auth?ck=${data.ck}&userId=${data.userId}`,
+ userId: data.userId,
+ }
+ }, [])
+
+ const handleOpenApp = useCallback(async () => {
+ const callbackUrl = await getCallbackUrl()
+ if (!callbackUrl) return
+ window.open(callbackUrl.url, "_top")
+ }, [getCallbackUrl])
+
+ const onceRef = useRef(false)
+ useEffect(() => {
+ if (isAuthenticated && !onceRef.current) {
+ handleOpenApp()
+ }
+ onceRef.current = true
+ }, [handleOpenApp, isAuthenticated])
+
+ const LoginOrStatusContent = useMemo(() => {
+ switch (true) {
+ case isAuthenticated: {
+ return (
+
+
+
+
+
+
+
+
+ {t("redirect.successMessage", { app_name: APP_NAME })}
+
+ {t("redirect.instruction", { app_name: APP_NAME })}
+
+
+
+
+
+
+
+ )
+ }
+ default: {
+ if (!authProviders?.credential) {
+ return (
+
+ {Object.entries(authProviders || [])
+ .filter(([key]) => key !== "credential")
+ .map(([key, provider]) => (
+
+ ))}
+
+ )
+ } else {
+ return (
+ <>
+
+
+
+ {Object.entries(authProviders || [])
+ .filter(([key]) => key !== "credential")
+ .map(([key, provider]) => (
+
{
+ loginHandler(key)
+ }}
+ >
+ {overrideProviderIconMap[provider.id] ? (
+
+ {overrideProviderIconMap[provider.id]}
+
+ ) : (
+
+ )}
+
+ ))}
+
+ >
+ )
+ }
+ }
+ }
+ }, [authProviders, handleOpenApp, isAuthenticated, refetch, t])
+ const Content = useMemo(() => {
+ switch (true) {
+ case redirecting: {
+ return {t("login.redirecting")}
+ }
+ default: {
+ return {LoginOrStatusContent}
+ }
+ }
+ }, [LoginOrStatusContent, redirecting, t])
+
+ return (
+
+
+ {isLoading &&
}
+
+
+ <>
+ {!isAuthenticated && !isLoading && (
+
+ {t("login.logInTo")}
+ {` ${APP_NAME}`}
+
+ )}
+ {Content}
+ >
+
+
+ )
+}
+
+const formSchema = z.object({
+ email: z.string().email(),
+ password: z.string().max(128),
+})
+
+async function onSubmit(values: z.infer) {
+ const res = await loginHandler("credential", "browser", values)
+ if (res?.error) {
+ toast.error(res.error.message)
+ return
+ }
+ queryClient.invalidateQueries({ queryKey: ["auth", "session"] })
+}
+
+function LoginWithPassword() {
+ const { t } = useTranslation("external")
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ email: "",
+ password: "",
+ },
+ })
+ const { isValid } = form.formState
+ const navigate = useNavigate()
+
+ return (
+
+
+ )
+}
diff --git a/apps/server/client/pages/(login)/forget-password.tsx b/apps/server/client/pages/(login)/forget-password.tsx
new file mode 100644
index 0000000000..7b1f800c83
--- /dev/null
+++ b/apps/server/client/pages/(login)/forget-password.tsx
@@ -0,0 +1,112 @@
+import { Button, MotionButtonBase } from "@follow/components/ui/button/index.jsx"
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@follow/components/ui/card/index.jsx"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@follow/components/ui/form/index.jsx"
+import { Input } from "@follow/components/ui/input/index.js"
+import { forgetPassword } from "@follow/shared/auth"
+import { env } from "@follow/shared/env"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { useMutation } from "@tanstack/react-query"
+import { useForm } from "react-hook-form"
+import { useTranslation } from "react-i18next"
+import { useNavigate } from "react-router"
+import { toast } from "sonner"
+import { z } from "zod"
+
+const forgetPasswordFormSchema = z.object({
+ email: z.string().email(),
+})
+
+export function Component() {
+ const { t } = useTranslation("external")
+ const form = useForm>({
+ resolver: zodResolver(forgetPasswordFormSchema),
+ defaultValues: {
+ email: "",
+ },
+ })
+
+ const { isValid } = form.formState
+ const updateMutation = useMutation({
+ mutationFn: async (values: z.infer) => {
+ const res = await forgetPassword({
+ email: values.email,
+ redirectTo: `${env.VITE_WEB_URL}/reset-password`,
+ })
+ if (res.error) {
+ throw new Error(res.error.message)
+ }
+ },
+ onError: (error) => {
+ toast.error(error.message)
+ },
+ onSuccess: () => {
+ toast.success(t("login.forget_password.success"))
+ },
+ })
+
+ function onSubmit(values: z.infer) {
+ updateMutation.mutate(values)
+ }
+
+ const navigate = useNavigate()
+
+ return (
+
+
+
+
+ {
+ history.length > 1 ? history.back() : navigate("/login")
+ }}
+ className="-ml-1 inline-flex cursor-pointer items-center"
+ >
+
+
+ {t("login.forget_password.label")}
+
+
+
+
+ {t("login.forget_password.description")}
+
+
+
+
+
+
+ )
+}
diff --git a/apps/server/client/pages/(login)/login.tsx b/apps/server/client/pages/(login)/login.tsx
index c0d537051f..280f89782e 100644
--- a/apps/server/client/pages/(login)/login.tsx
+++ b/apps/server/client/pages/(login)/login.tsx
@@ -1,135 +1,5 @@
-import { UserAvatar } from "@client/components/ui/user-avatar"
-import { useSession } from "@client/query/auth"
-import { useAuthProviders } from "@client/query/users"
-import { Logo } from "@follow/components/icons/logo.jsx"
-import { Button } from "@follow/components/ui/button/index.js"
-import { authProvidersConfig } from "@follow/constants"
-import { createSession, loginHandler, signOut } from "@follow/shared/auth"
-import { DEEPLINK_SCHEME } from "@follow/shared/constants"
-import { cn } from "@follow/utils/utils"
-import { useCallback, useEffect, useRef, useState } from "react"
-import { useTranslation } from "react-i18next"
-import { useLocation } from "react-router"
+import { Login } from "@client/modules/login"
export function Component() {
return
}
-
-function Login() {
- const { status, refetch } = useSession()
-
- const [redirecting, setRedirecting] = useState(false)
-
- const { data: authProviders } = useAuthProviders()
- const location = useLocation()
- const urlParams = new URLSearchParams(location.search)
- const provider = urlParams.get("provider")
-
- const isAuthenticated = status === "authenticated"
-
- const { t } = useTranslation("external")
-
- useEffect(() => {
- if (provider && status === "unauthenticated") {
- loginHandler(provider)
- setRedirecting(true)
- }
- }, [status])
-
- const getCallbackUrl = useCallback(async () => {
- const { data } = await createSession()
- if (!data) return null
- return {
- url: `${DEEPLINK_SCHEME}auth?ck=${data.ck}&userId=${data.userId}`,
- userId: data.userId,
- }
- }, [])
-
- const handleOpenApp = useCallback(async () => {
- const callbackUrl = await getCallbackUrl()
- if (!callbackUrl) return
- window.open(callbackUrl.url, "_top")
- }, [getCallbackUrl])
-
- const onceRef = useRef(false)
- useEffect(() => {
- if (isAuthenticated && !onceRef.current) {
- handleOpenApp()
- }
- onceRef.current = true
- }, [handleOpenApp, isAuthenticated])
-
- return (
-
-
- {!isAuthenticated && (
-
- {t("login.logInTo")}
- {` ${APP_NAME}`}
-
- )}
- {redirecting ? (
-
{t("login.redirecting")}
- ) : (
-
- {isAuthenticated ? (
-
-
-
-
-
-
-
-
- {t("redirect.successMessage", { app_name: APP_NAME })}
-
- {t("redirect.instruction", { app_name: APP_NAME })}
-
-
-
-
-
-
-
- ) : (
- <>
- {Object.entries(authProviders || []).map(([key, provider]) => (
-
- ))}
- >
- )}
-
- )}
-
- )
-}
diff --git a/apps/server/client/pages/(login)/register.tsx b/apps/server/client/pages/(login)/register.tsx
new file mode 100644
index 0000000000..9066b8a657
--- /dev/null
+++ b/apps/server/client/pages/(login)/register.tsx
@@ -0,0 +1,139 @@
+import { Logo } from "@follow/components/icons/logo.jsx"
+import { Button } from "@follow/components/ui/button/index.jsx"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@follow/components/ui/form/index.jsx"
+import { Input } from "@follow/components/ui/input/index.js"
+import { signUp } from "@follow/shared/auth"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { useForm } from "react-hook-form"
+import { Trans, useTranslation } from "react-i18next"
+import { Link, useNavigate } from "react-router"
+import { toast } from "sonner"
+import { z } from "zod"
+
+export function Component() {
+ return (
+
+
+
+
+ )
+}
+
+const formSchema = z
+ .object({
+ email: z.string().email(),
+ password: z.string().min(8).max(128),
+ confirmPassword: z.string(),
+ })
+ .refine((data) => data.password === data.confirmPassword, {
+ message: "Passwords don't match",
+ path: ["confirmPassword"],
+ })
+
+function RegisterForm() {
+ const { t } = useTranslation("external")
+
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ email: "",
+ password: "",
+ confirmPassword: "",
+ },
+ })
+
+ const { isValid } = form.formState
+
+ const navigate = useNavigate()
+
+ function onSubmit(values: z.infer) {
+ return signUp.email({
+ email: values.email,
+ password: values.password,
+ name: values.email.split("@")[0],
+ callbackURL: "/",
+ fetchOptions: {
+ onSuccess() {
+ navigate("/login")
+ },
+ onError(context) {
+ toast.error(context.error.message)
+ },
+ },
+ })
+ }
+
+ return (
+
+ )
+}
diff --git a/apps/server/client/pages/(login)/reset-password.tsx b/apps/server/client/pages/(login)/reset-password.tsx
new file mode 100644
index 0000000000..2dac23add9
--- /dev/null
+++ b/apps/server/client/pages/(login)/reset-password.tsx
@@ -0,0 +1,130 @@
+import { Button, MotionButtonBase } from "@follow/components/ui/button/index.jsx"
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@follow/components/ui/card/index.jsx"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@follow/components/ui/form/index.jsx"
+import { Input } from "@follow/components/ui/input/index.js"
+import { resetPassword } from "@follow/shared/auth"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { useMutation } from "@tanstack/react-query"
+import { useForm } from "react-hook-form"
+import { useTranslation } from "react-i18next"
+import { useNavigate } from "react-router"
+import { toast } from "sonner"
+import { z } from "zod"
+
+const passwordSchema = z.string().min(8).max(128)
+const initPasswordFormSchema = z
+ .object({
+ newPassword: passwordSchema,
+ confirmPassword: passwordSchema,
+ })
+ .refine((data) => data.newPassword === data.confirmPassword, {
+ message: "Passwords don't match",
+ path: ["confirmPassword"],
+ })
+
+export function Component() {
+ const { t } = useTranslation("external")
+ const form = useForm>({
+ resolver: zodResolver(initPasswordFormSchema),
+ defaultValues: {
+ newPassword: "",
+ confirmPassword: "",
+ },
+ })
+
+ const { isValid } = form.formState
+
+ const navigate = useNavigate()
+ const updateMutation = useMutation({
+ mutationFn: async (values: z.infer) => {
+ const res = await resetPassword({ newPassword: values.newPassword })
+ const error = res.error?.message
+ if (error) {
+ throw new Error(error)
+ }
+ },
+ onError: (error) => {
+ toast.error(error.message)
+ },
+ onSuccess: () => {
+ toast.success(t("login.reset_password.success"))
+ navigate("/login")
+ },
+ })
+
+ function onSubmit(values: z.infer) {
+ updateMutation.mutate(values)
+ }
+
+ return (
+
+ )
+}
diff --git a/apps/server/client/query/users.ts b/apps/server/client/query/users.ts
index 1c2e155311..cdebcba5d2 100644
--- a/apps/server/client/query/users.ts
+++ b/apps/server/client/query/users.ts
@@ -56,20 +56,15 @@ export const useUserQuery = (handleOrId: string | undefined) => {
initialData: getHydrateData(`profiles.$get,query:id=${handleOrId}`),
})
}
-
+export interface AuthProvider {
+ name: string
+ id: string
+ color: string
+ icon: string
+}
export const useAuthProviders = () => {
return useQuery({
queryKey: ["providers"],
- queryFn: async () => (await getProviders()).data,
- placeholderData: {
- google: {
- id: "google",
- name: "Google",
- },
- github: {
- id: "github",
- name: "GitHub",
- },
- },
+ queryFn: async () => (await getProviders()).data as Record,
})
}
diff --git a/apps/server/package.json b/apps/server/package.json
index 57a03362e6..b4d5898a2f 100644
--- a/apps/server/package.json
+++ b/apps/server/package.json
@@ -27,6 +27,7 @@
"rc-modal-sheet": "0.3.2",
"react": "^18.3.1",
"react-blurhash": "^0.3.0",
+ "react-hook-form": "7.54.0",
"react-hotkeys-hook": "4.6.1",
"react-i18next": "^15.1.3",
"react-photo-view": "1.2.6",
@@ -34,7 +35,8 @@
"satori": "0.12.0",
"sonner": "1.7.1",
"use-context-selector": "2.0.0",
- "xss": "1.0.15"
+ "xss": "1.0.15",
+ "zod": "3.23.8"
},
"devDependencies": {
"@follow/components": "workspace:*",
diff --git a/changelog/next.md b/changelog/next.md
index 17888d80b6..d7f8b9d6fb 100644
--- a/changelog/next.md
+++ b/changelog/next.md
@@ -2,6 +2,8 @@
## New Features
+- Register or Login with email and password
+
## Improvements
## Bug Fixes
diff --git a/locales/external/de.json b/locales/external/de.json
index 40b9c343a9..9c662ce994 100644
--- a/locales/external/de.json
+++ b/locales/external/de.json
@@ -15,7 +15,7 @@
"invitation.getCodeMessage": "You can get an invitation code through the following ways:",
"invitation.title": "Invitation Code",
"login.backToWebApp": "Back to the web app",
- "login.logInTo": "Log in to ",
+ "login.logInTo": "Login to ",
"login.openApp": "Open app",
"login.redirecting": "Redirecting",
"login.welcomeTo": "Welcome to ",
diff --git a/locales/external/en.json b/locales/external/en.json
index ac5ff6e0d8..a0ac35eb35 100644
--- a/locales/external/en.json
+++ b/locales/external/en.json
@@ -34,14 +34,37 @@
"invitation.getCodeMessage": "You can get an invitation code in the following ways:",
"invitation.title": "Invitation Code",
"login.backToWebApp": "Back To Web App",
+ "login.confirm_password.label": "Confirm Password",
"login.continueWith": "Continue with {{provider}}",
- "login.logInTo": "Log in to ",
+ "login.email": "Email",
+ "login.forget_password.description": "Enter the email address associated with your account and we’ll send you an email about how to reset your password.",
+ "login.forget_password.label": "Forget Password",
+ "login.forget_password.note": "Forgot your password?",
+ "login.forget_password.success": "Email has been sent successfully",
+ "login.logInTo": "Login to ",
+ "login.logInWithEmail": "Login with email",
+ "login.new_password.label": "New Password",
"login.openApp": "Open App",
+ "login.or": "Or",
+ "login.password": "Password",
"login.redirecting": "Redirecting",
+ "login.register": "Create one",
+ "login.reset_password.description": "Enter new password and confirm it to reset your password",
+ "login.reset_password.label": "Reset Password",
+ "login.reset_password.success": "Password has been successfully reset",
"login.signOut": "Sign out",
+ "login.signUp": "Sign up with email",
+ "login.submit": "Submit",
"login.welcomeTo": "Welcome to ",
"redirect.continueInBrowser": "Continue in Browser",
"redirect.instruction": "Now is the time to open {{app_name}} and safely close this page.",
"redirect.openApp": "Open {{app_name}}",
- "redirect.successMessage": "You have successfully connected to {{app_name}} Account."
+ "redirect.successMessage": "You have successfully connected to {{app_name}} Account.",
+ "register.confirm_password": "Confirm Password",
+ "register.email": "Email",
+ "register.label": "Create a {{app_name}} account",
+ "register.login": "Login",
+ "register.note": "Already have an account? ",
+ "register.password": "Password",
+ "register.submit": "Create account"
}
diff --git a/locales/settings/en.json b/locales/settings/en.json
index 7c2788fe4f..1c770ab962 100644
--- a/locales/settings/en.json
+++ b/locales/settings/en.json
@@ -259,14 +259,21 @@
"lists.title": "Title",
"lists.view": "View",
"profile.avatar.label": "Avatar",
+ "profile.change_password.label": "Change Password",
+ "profile.confirm_password.label": "Confirm Password",
+ "profile.current_password.label": "Current Password",
+ "profile.email.label": "Email",
"profile.handle.description": "Your unique identifier.",
"profile.handle.label": "Handle",
"profile.name.description": "Your public display name.",
"profile.name.label": "Display Name",
+ "profile.new_password.label": "New Password",
+ "profile.reset_password_mail_sent": "Reset password mail sent.",
"profile.sidebar_title": "Profile",
"profile.submit": "Submit",
"profile.title": "Profile Settings",
"profile.updateSuccess": "Profile updated.",
+ "profile.update_password_success": "Password updated.",
"titles.about": "About",
"titles.actions": "Actions",
"titles.appearance": "Appearance",
diff --git a/packages/shared/src/auth.ts b/packages/shared/src/auth.ts
index 7e71ff759c..089292a818 100644
--- a/packages/shared/src/auth.ts
+++ b/packages/shared/src/auth.ts
@@ -19,6 +19,7 @@ const serverPlugins = [
user: {
handle: {
type: "string",
+ required: false,
},
},
}),
@@ -29,14 +30,44 @@ const authClient = createAuthClient({
plugins: serverPlugins,
})
-export const { signIn, signOut, getSession, getProviders, createSession, updateUser } = authClient
+// @keep-sorted
+export const {
+ changePassword,
+ createSession,
+ forgetPassword,
+ getProviders,
+ getSession,
+ linkSocial,
+ listAccounts,
+ resetPassword,
+ signIn,
+ signOut,
+ signUp,
+ updateUser,
+} = authClient
export const LOGIN_CALLBACK_URL = `${WEB_URL}/login`
export type LoginRuntime = "browser" | "app"
-export const loginHandler = (provider: string, runtime: LoginRuntime = "app") => {
+export const loginHandler = async (
+ provider: string,
+ runtime?: LoginRuntime,
+ args?: {
+ email?: string
+ password?: string
+ },
+) => {
+ const { email, password } = args ?? {}
if (IN_ELECTRON) {
window.open(`${WEB_URL}/login?provider=${provider}`)
} else {
+ if (provider === "credential") {
+ if (!email || !password) {
+ window.location.href = "/login"
+ return
+ }
+ return signIn.email({ email, password })
+ }
+
signIn.social({
provider: provider as "google" | "github" | "apple",
callbackURL: runtime === "app" ? LOGIN_CALLBACK_URL : undefined,
diff --git a/packages/shared/src/hono.ts b/packages/shared/src/hono.ts
index f009cb30c9..738643bb77 100644
--- a/packages/shared/src/hono.ts
+++ b/packages/shared/src/hono.ts
@@ -341,7 +341,32 @@ declare const actions: drizzle_orm_pg_core.PgTableWithColumns<{
baseColumn: never;
identity: undefined;
generated: undefined;
- }, {}, {}>;
+ }, {}, {
+ $type: {
+ name: string;
+ condition: ConditionItem[] | ConditionItem[][];
+ result: {
+ disabled?: boolean;
+ translation?: z.infer;
+ summary?: boolean;
+ readability?: boolean;
+ sourceContent?: boolean;
+ silence?: boolean;
+ block?: boolean;
+ newEntryNotification?: boolean;
+ rewriteRules?: {
+ from: string;
+ to: string;
+ }[];
+ blockRules?: {
+ field: z.infer;
+ operator: z.infer;
+ value: string | number;
+ }[];
+ webhooks?: string[];
+ };
+ }[];
+ }>;
};
dialect: "pg";
}>;
@@ -1004,7 +1029,38 @@ declare const airdrops: drizzle_orm_pg_core.PgTableWithColumns<{
baseColumn: never;
identity: undefined;
generated: undefined;
- }, {}, {}>;
+ }, {}, {
+ $type: {
+ "Invitations count": number;
+ "Purchase lists cost": number;
+ "Total tip amount": number;
+ "Feeds subscriptions count": number;
+ "Lists subscriptions count": number;
+ "Inbox subscriptions count": number;
+ "Recent read count in the last month": number;
+ "Mint count": number;
+ "Claimed feeds count": number;
+ "Claimed feeds subscriptions count": number;
+ "Lists with more than 1 feed count": number;
+ "Created lists subscriptions count": number;
+ "Created lists income amount": number;
+ "GitHub Community Contributions": number;
+ "Invitations count Rank": number;
+ "Purchase lists cost Rank": number;
+ "Total tip amount Rank": number;
+ "Feeds subscriptions count Rank": number;
+ "Lists subscriptions count Rank": number;
+ "Inbox subscriptions count Rank": number;
+ "Recent read count in the last month Rank": number;
+ "Mint count Rank": number;
+ "Claimed feeds count Rank": number;
+ "Claimed feeds subscriptions count Rank": number;
+ "Lists with more than 1 feed count Rank": number;
+ "Created lists subscriptions count Rank": number;
+ "Created lists income amount Rank": number;
+ "GitHub Community Contributions Rank": number;
+ } | null;
+ }>;
verify: drizzle_orm_pg_core.PgColumn<{
name: "verify";
tableName: "airdrops";
@@ -1485,6 +1541,15 @@ declare const CommonEntryFields: {
data: string[];
driverParam: string | string[];
enumValues: [string, ...string[]];
+ size: undefined;
+ baseBuilder: {
+ name: "categories";
+ dataType: "string";
+ columnType: "PgText";
+ data: string;
+ enumValues: [string, ...string[]];
+ driverParam: string;
+ };
}, {
name: "categories";
dataType: "string";
@@ -1711,7 +1776,9 @@ declare const entries: drizzle_orm_pg_core.PgTableWithColumns<{
baseColumn: never;
identity: undefined;
generated: undefined;
- }, {}, {}>;
+ }, {}, {
+ $type: MediaModel[];
+ }>;
categories: drizzle_orm_pg_core.PgColumn<{
name: "categories";
tableName: "entries";
@@ -1741,10 +1808,20 @@ declare const entries: drizzle_orm_pg_core.PgTableWithColumns<{
baseColumn: never;
identity: undefined;
generated: undefined;
- }, object, object>;
+ }, {}, {}>;
identity: undefined;
generated: undefined;
- }, {}, {}>;
+ }, {}, {
+ baseBuilder: drizzle_orm_pg_core.PgColumnBuilder<{
+ name: "categories";
+ dataType: "string";
+ columnType: "PgText";
+ data: string;
+ enumValues: [string, ...string[]];
+ driverParam: string;
+ }, {}, {}, drizzle_orm.ColumnBuilderExtraConfig>;
+ size: undefined;
+ }>;
attachments: drizzle_orm_pg_core.PgColumn<{
name: "attachments";
tableName: "entries";
@@ -1761,7 +1838,9 @@ declare const entries: drizzle_orm_pg_core.PgTableWithColumns<{
baseColumn: never;
identity: undefined;
generated: undefined;
- }, {}, {}>;
+ }, {}, {
+ $type: AttachmentsModel[];
+ }>;
extra: drizzle_orm_pg_core.PgColumn<{
name: "extra";
tableName: "entries";
@@ -1778,7 +1857,9 @@ declare const entries: drizzle_orm_pg_core.PgTableWithColumns<{
baseColumn: never;
identity: undefined;
generated: undefined;
- }, {}, {}>;
+ }, {}, {
+ $type: ExtraModel;
+ }>;
language: drizzle_orm_pg_core.PgColumn<{
name: "language";
tableName: "entries";
@@ -2250,10 +2331,20 @@ declare const entryReadHistories: drizzle_orm_pg_core.PgTableWithColumns<{
baseColumn: never;
identity: undefined;
generated: undefined;
- }, object, object>;
+ }, {}, {}>;
identity: undefined;
generated: undefined;
- }, {}, {}>;
+ }, {}, {
+ baseBuilder: drizzle_orm_pg_core.PgColumnBuilder<{
+ name: "user_ids";
+ dataType: "string";
+ columnType: "PgText";
+ data: string;
+ enumValues: [string, ...string[]];
+ driverParam: string;
+ }, {}, {}, drizzle_orm.ColumnBuilderExtraConfig>;
+ size: undefined;
+ }>;
readCount: drizzle_orm_pg_core.PgColumn<{
name: "read_count";
tableName: "entryReadHistories";
@@ -3112,7 +3203,9 @@ declare const inboxesEntries: drizzle_orm_pg_core.PgTableWithColumns<{
baseColumn: never;
identity: undefined;
generated: undefined;
- }, {}, {}>;
+ }, {}, {
+ $type: MediaModel[];
+ }>;
categories: drizzle_orm_pg_core.PgColumn<{
name: "categories";
tableName: "inboxes_entries";
@@ -3142,10 +3235,20 @@ declare const inboxesEntries: drizzle_orm_pg_core.PgTableWithColumns<{
baseColumn: never;
identity: undefined;
generated: undefined;
- }, object, object>;
+ }, {}, {}>;
identity: undefined;
generated: undefined;
- }, {}, {}>;
+ }, {}, {
+ baseBuilder: drizzle_orm_pg_core.PgColumnBuilder<{
+ name: "categories";
+ dataType: "string";
+ columnType: "PgText";
+ data: string;
+ enumValues: [string, ...string[]];
+ driverParam: string;
+ }, {}, {}, drizzle_orm.ColumnBuilderExtraConfig>;
+ size: undefined;
+ }>;
attachments: drizzle_orm_pg_core.PgColumn<{
name: "attachments";
tableName: "inboxes_entries";
@@ -3162,7 +3265,9 @@ declare const inboxesEntries: drizzle_orm_pg_core.PgTableWithColumns<{
baseColumn: never;
identity: undefined;
generated: undefined;
- }, {}, {}>;
+ }, {}, {
+ $type: AttachmentsModel[];
+ }>;
extra: drizzle_orm_pg_core.PgColumn<{
name: "extra";
tableName: "inboxes_entries";
@@ -3179,7 +3284,9 @@ declare const inboxesEntries: drizzle_orm_pg_core.PgTableWithColumns<{
baseColumn: never;
identity: undefined;
generated: undefined;
- }, {}, {}>;
+ }, {}, {
+ $type: ExtraModel;
+ }>;
language: drizzle_orm_pg_core.PgColumn<{
name: "language";
tableName: "inboxes_entries";
@@ -4117,10 +4224,20 @@ declare const lists: drizzle_orm_pg_core.PgTableWithColumns<{
baseColumn: never;
identity: undefined;
generated: undefined;
- }, object, object>;
+ }, {}, {}>;
identity: undefined;
generated: undefined;
- }, {}, {}>;
+ }, {}, {
+ baseBuilder: drizzle_orm_pg_core.PgColumnBuilder<{
+ name: "feed_ids";
+ dataType: "string";
+ columnType: "PgText";
+ data: string;
+ enumValues: [string, ...string[]];
+ driverParam: string;
+ }, {}, {}, drizzle_orm.ColumnBuilderExtraConfig>;
+ size: undefined;
+ }>;
title: drizzle_orm_pg_core.PgColumn<{
name: "title";
tableName: "lists";
@@ -4710,7 +4827,9 @@ declare const settings: drizzle_orm_pg_core.PgTableWithColumns<{
baseColumn: never;
identity: undefined;
generated: undefined;
- }, {}, {}>;
+ }, {}, {
+ $type: Record;
+ }>;
updateAt: drizzle_orm_pg_core.PgColumn<{
name: "update_at";
tableName: "settings";
@@ -5036,16 +5155,16 @@ declare const users: drizzle_orm_pg_core.PgTableWithColumns<{
dialect: "pg";
}>;
declare function lower(handle: AnyPgColumn): SQL;
-declare const usersOpenApiSchema: z.ZodObject;
- email: z.ZodString;
- emailVerified: z.ZodNullable;
- image: z.ZodNullable;
- handle: z.ZodNullable;
- createdAt: z.ZodDate;
- updatedAt: z.ZodDate;
-}, "email">, z.UnknownKeysParam, z.ZodTypeAny, {
+declare const usersOpenApiSchema: zod.ZodObject;
+ email: zod.ZodString;
+ emailVerified: zod.ZodNullable;
+ image: zod.ZodNullable;
+ handle: zod.ZodNullable;
+ createdAt: zod.ZodDate;
+ updatedAt: zod.ZodDate;
+}, "email">, "strip", zod.ZodTypeAny, {
name: string | null;
id: string;
emailVerified: boolean | null;
@@ -6203,12 +6322,13 @@ declare const boosts: drizzle_orm_pg_core.PgTableWithColumns<{
declare const auth: {
handler: (request: Request) => Promise;
api: {
- getSession: ((context: {
+ getSession: (context: {
headers: Headers;
query?: {
disableCookieCache?: boolean;
} | undefined;
- }) => Promise<{
+ asResponse?: R | undefined;
+ }) => false extends R ? Promise<{
user: {
id: string;
createdAt: Date;
@@ -6239,17 +6359,17 @@ declare const auth: {
toUserId: string | null;
} | undefined;
role: "user" | "trial";
- } | null>) & {
+ } | null> & {
options: {
method: "GET";
query: zod.ZodOptional;
+ disableCookieCache: zod.ZodOptional]>>;
disableRefresh: zod.ZodOptional;
}, "strip", zod.ZodTypeAny, {
disableCookieCache?: boolean | undefined;
disableRefresh?: boolean | undefined;
}, {
- disableCookieCache?: boolean | undefined;
+ disableCookieCache?: string | boolean | undefined;
disableRefresh?: boolean | undefined;
}>>;
requireHeaders: true;
@@ -6295,9 +6415,19 @@ declare const auth: {
metadata: {
CUSTOM_SESSION: boolean;
};
+ query: zod.ZodOptional]>>;
+ disableRefresh: zod.ZodOptional;
+ }, "strip", zod.ZodTypeAny, {
+ disableCookieCache?: boolean | undefined;
+ disableRefresh?: boolean | undefined;
+ }, {
+ disableCookieCache?: string | boolean | undefined;
+ disableRefresh?: boolean | undefined;
+ }>>;
};
path: "/get-session";
- };
+ } : Promise;
} & {
getProviders: {
>;
body: zod.ZodObject<{
callbackURL: zod.ZodOptional;
+ newUserCallbackURL: zod.ZodOptional;
errorCallbackURL: zod.ZodOptional;
- provider: zod.ZodEnum<["github", ...("apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab")[]]>;
+ provider: zod.ZodEnum<["github", ...("apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab" | "reddit")[]]>;
disableRedirect: zod.ZodOptional;
idToken: zod.ZodOptional>;
}, "strip", zod.ZodTypeAny, {
- provider: "apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab";
+ provider: "apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab" | "reddit";
idToken?: {
token: string;
accessToken?: string | undefined;
@@ -6385,10 +6516,11 @@ declare const auth: {
nonce?: string | undefined;
} | undefined;
callbackURL?: string | undefined;
+ newUserCallbackURL?: string | undefined;
errorCallbackURL?: string | undefined;
disableRedirect?: boolean | undefined;
}, {
- provider: "apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab";
+ provider: "apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab" | "reddit";
idToken?: {
token: string;
accessToken?: string | undefined;
@@ -6397,6 +6529,7 @@ declare const auth: {
nonce?: string | undefined;
} | undefined;
callbackURL?: string | undefined;
+ newUserCallbackURL?: string | undefined;
errorCallbackURL?: string | undefined;
disableRedirect?: boolean | undefined;
}>;
@@ -6472,8 +6605,9 @@ declare const auth: {
}>>;
body: zod.ZodObject<{
callbackURL: zod.ZodOptional;
+ newUserCallbackURL: zod.ZodOptional;
errorCallbackURL: zod.ZodOptional;
- provider: zod.ZodEnum<["github", ...("apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab")[]]>;
+ provider: zod.ZodEnum<["github", ...("apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab" | "reddit")[]]>;
disableRedirect: zod.ZodOptional;
idToken: zod.ZodOptional>;
}, "strip", zod.ZodTypeAny, {
- provider: "apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab";
+ provider: "apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab" | "reddit";
idToken?: {
token: string;
accessToken?: string | undefined;
@@ -6504,10 +6638,11 @@ declare const auth: {
nonce?: string | undefined;
} | undefined;
callbackURL?: string | undefined;
+ newUserCallbackURL?: string | undefined;
errorCallbackURL?: string | undefined;
disableRedirect?: boolean | undefined;
}, {
- provider: "apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab";
+ provider: "apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab" | "reddit";
idToken?: {
token: string;
accessToken?: string | undefined;
@@ -6516,6 +6651,7 @@ declare const auth: {
nonce?: string | undefined;
} | undefined;
callbackURL?: string | undefined;
+ newUserCallbackURL?: string | undefined;
errorCallbackURL?: string | undefined;
disableRedirect?: boolean | undefined;
}>;
@@ -6684,11 +6820,25 @@ declare const auth: {
schema: {
type: "object";
properties: {
- user: {
+ id: {
type: string;
+ description: string;
};
- session: {
+ email: {
+ type: string;
+ description: string;
+ };
+ name: {
+ type: string;
+ description: string;
+ };
+ image: {
+ type: string;
+ description: string;
+ };
+ emailVerified: {
type: string;
+ description: string;
};
};
};
@@ -6701,38 +6851,11 @@ declare const auth: {
}>]>(...ctx: C): Promise;
path: "/sign-up/email";
options: {
@@ -6802,11 +6925,25 @@ declare const auth: {
schema: {
type: "object";
properties: {
- user: {
+ id: {
+ type: string;
+ description: string;
+ };
+ email: {
type: string;
+ description: string;
};
- session: {
+ name: {
type: string;
+ description: string;
+ };
+ image: {
+ type: string;
+ description: string;
+ };
+ emailVerified: {
+ type: string;
+ description: string;
};
};
};
@@ -6850,9 +6987,6 @@ declare const auth: {
schema: {
type: "object";
properties: {
- session: {
- type: string;
- };
user: {
type: string;
};
@@ -6876,22 +7010,12 @@ declare const auth: {
}] ? Response : {
user: {
id: string;
- createdAt: Date;
- updatedAt: Date;
email: string;
- emailVerified: boolean;
name: string;
- image?: string | null | undefined;
- };
- session: {
- id: string;
- userId: string;
+ image: string | null | undefined;
+ emailVerified: boolean;
createdAt: Date;
updatedAt: Date;
- expiresAt: Date;
- token: string;
- ipAddress?: string | null | undefined;
- userAgent?: string | null | undefined;
};
redirect: boolean;
url: string | undefined;
@@ -6926,9 +7050,6 @@ declare const auth: {
schema: {
type: "object";
properties: {
- session: {
- type: string;
- };
user: {
type: string;
};
@@ -7806,7 +7927,13 @@ declare const auth: {
}>]>(...ctx: C): Promise;
path: "/update-user";
options: {
@@ -8540,12 +8667,12 @@ declare const auth: {
}>>;
body: zod.ZodObject<{
callbackURL: zod.ZodOptional;
- provider: zod.ZodEnum<["github", ...("apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab")[]]>;
+ provider: zod.ZodEnum<["github", ...("apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab" | "reddit")[]]>;
}, "strip", zod.ZodTypeAny, {
- provider: "apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab";
+ provider: "apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab" | "reddit";
callbackURL?: string | undefined;
}, {
- provider: "apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab";
+ provider: "apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab" | "reddit";
callbackURL?: string | undefined;
}>;
use: better_call.Endpoint>;
body: zod.ZodObject<{
callbackURL: zod.ZodOptional;
- provider: zod.ZodEnum<["github", ...("apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab")[]]>;
+ provider: zod.ZodEnum<["github", ...("apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab" | "reddit")[]]>;
}, "strip", zod.ZodTypeAny, {
- provider: "apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab";
+ provider: "apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab" | "reddit";
callbackURL?: string | undefined;
}, {
- provider: "apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab";
+ provider: "apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab" | "reddit";
callbackURL?: string | undefined;
}>;
use: better_call.Endpoint;
};
plugins: ({
id: "custom-session";
@@ -8915,6 +9047,16 @@ declare const auth: {
metadata: {
CUSTOM_SESSION: boolean;
};
+ query: zod.ZodOptional]>>;
+ disableRefresh: zod.ZodOptional;
+ }, "strip", zod.ZodTypeAny, {
+ disableCookieCache?: boolean | undefined;
+ disableRefresh?: boolean | undefined;
+ }, {
+ disableCookieCache?: string | boolean | undefined;
+ disableRefresh?: boolean | undefined;
+ }>>;
}> | undefined)?]>(...ctx: C): Promise]>>;
+ disableRefresh: zod.ZodOptional;
+ }, "strip", zod.ZodTypeAny, {
+ disableCookieCache?: boolean | undefined;
+ disableRefresh?: boolean | undefined;
+ }, {
+ disableCookieCache?: string | boolean | undefined;
+ disableRefresh?: boolean | undefined;
+ }>>;
};
method: better_call.Method | better_call.Method[];
headers: Headers;
@@ -9041,6 +9193,28 @@ declare const auth: {
};
};
};
+ $ErrorCodes: {
+ USER_NOT_FOUND: string;
+ FAILED_TO_CREATE_USER: string;
+ FAILED_TO_CREATE_SESSION: string;
+ FAILED_TO_UPDATE_USER: string;
+ FAILED_TO_GET_SESSION: string;
+ INVALID_PASSWORD: string;
+ INVALID_EMAIL: string;
+ INVALID_EMAIL_OR_PASSWORD: string;
+ SOCIAL_ACCOUNT_ALREADY_LINKED: string;
+ PROVIDER_NOT_FOUND: string;
+ INVALID_TOKEN: string;
+ ID_TOKEN_NOT_SUPPORTED: string;
+ FAILED_TO_GET_USER_INFO: string;
+ USER_EMAIL_NOT_FOUND: string;
+ EMAIL_NOT_VERIFIED: string;
+ PASSWORD_TOO_SHORT: string;
+ PASSWORD_TOO_LONG: string;
+ USER_ALREADY_EXISTS: string;
+ EMAIL_CAN_NOT_BE_UPDATED: string;
+ CREDENTIAL_ACCOUNT_NOT_FOUND: string;
+ };
};
type AuthSession = Awaited>;
@@ -9089,7 +9263,7 @@ declare const _routes: hono_hono_base.HonoBase