From 883f4d43a61a5ee7bd2fe61bc19f92bffecd0f00 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Mon, 9 Dec 2024 22:31:25 +0800 Subject: [PATCH 01/36] feat: login with password --- .../src/modules/auth/LoginModalContent.tsx | 4 +- .../modules/profile/update-password-form.tsx | 178 ++++++++++ .../src/pages/settings/(settings)/profile.tsx | 2 + apps/renderer/src/queries/auth.ts | 19 +- apps/server/client/pages/(login)/login.tsx | 67 +++- locales/errors/en.json | 1 + locales/external/en.json | 2 + locales/settings/en.json | 3 + packages/constants/src/auth-providers.ts | 5 + packages/shared/src/auth.ts | 26 +- packages/shared/src/hono.ts | 321 ++++++++++++------ 11 files changed, 494 insertions(+), 134 deletions(-) create mode 100644 apps/renderer/src/modules/profile/update-password-form.tsx diff --git a/apps/renderer/src/modules/auth/LoginModalContent.tsx b/apps/renderer/src/modules/auth/LoginModalContent.tsx index 7c30b673fe..f172191c3d 100644 --- a/apps/renderer/src/modules/auth/LoginModalContent.tsx +++ b/apps/renderer/src/modules/auth/LoginModalContent.tsx @@ -106,7 +106,7 @@ export const LoginModalContent = (props: LoginModalContentProps) => { )} disabled={disabled} onClick={() => { - loginHandler(provider.id, runtime) + loginHandler(provider.id, { runtime }) setLoadingLockSet(provider.id) window.analytics?.capture("login", { type: provider.id, @@ -200,7 +200,7 @@ export const AuthProvidersRender: FC<{ disabled={authProcessingLockSet.has(provider.id)} onClick={() => { if (authProcessingLockSet.has(provider.id)) return - loginHandler(provider.id, runtime) + loginHandler(provider.id, { runtime }) setAuthProcessingLockSet((prev) => { prev.add(provider.id) diff --git a/apps/renderer/src/modules/profile/update-password-form.tsx b/apps/renderer/src/modules/profile/update-password-form.tsx new file mode 100644 index 0000000000..a0693fb543 --- /dev/null +++ b/apps/renderer/src/modules/profile/update-password-form.tsx @@ -0,0 +1,178 @@ +import { Button } from "@follow/components/ui/button/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 { changePassword } 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 { toast } from "sonner" +import { z } from "zod" + +import { apiClient } from "~/lib/api-fetch" +import { toastFetchError } from "~/lib/error-parser" +import { Queries } from "~/queries" +import { useHasPassword } from "~/queries/auth" + +const passwordSchema = z.string().min(8).max(128) +const initPasswordFormSchema = z.object({ + password: passwordSchema, +}) + +const InitPasswordForm = () => { + const { t } = useTranslation("settings") + + const form = useForm>({ + resolver: zodResolver(initPasswordFormSchema), + defaultValues: { + password: "", + }, + }) + + const updateMutation = useMutation({ + mutationFn: async (values: z.infer) => { + await apiClient["auth-app"]["init-password"].$patch({ + json: values, + }) + await Queries.auth.getAccounts().invalidate() + }, + onError: (error) => { + toastFetchError(error) + }, + onSuccess: (_) => { + toast(t("profile.update_password_success"), { + duration: 3000, + }) + form.reset() + }, + }) + + function onSubmit(values: z.infer) { + updateMutation.mutate(values) + } + + return ( +
+ + ( + + {t("profile.new_password.label")} + + + + + + )} + /> + +
+ +
+ + + ) +} + +const updatePasswordFormSchema = z.object({ + currentPassword: passwordSchema, + newPassword: passwordSchema, +}) + +const UpdateExistingPasswordForm = () => { + const { t } = useTranslation("settings") + + const form = useForm>({ + resolver: zodResolver(updatePasswordFormSchema), + defaultValues: { + currentPassword: "", + newPassword: "", + }, + }) + + const updateMutation = useMutation({ + mutationFn: async (values: z.infer) => { + const res = await changePassword(values) + if (res.error) { + throw new Error(res.error.message) + } + }, + onError: (error) => { + toast.error(error.message) + }, + onSuccess: (_) => { + toast(t("profile.update_password_success"), { + duration: 3000, + }) + form.reset() + }, + }) + + function onSubmit(values: z.infer) { + updateMutation.mutate(values) + } + + return ( +
+ + ( + + {t("profile.current_password.label")} + + + + + + + )} + /> + ( + + {t("profile.new_password.label")} + + + + + + + )} + /> +
+ +
+ + + ) +} + +export const UpdatePasswordForm = () => { + const { data: hasPassword, isLoading } = useHasPassword() + + if (isLoading) { + return null + } + + if (!hasPassword) { + return + } + 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/pages/(login)/login.tsx b/apps/server/client/pages/(login)/login.tsx index c0d537051f..6609670c61 100644 --- a/apps/server/client/pages/(login)/login.tsx +++ b/apps/server/client/pages/(login)/login.tsx @@ -1,8 +1,10 @@ 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 { Button } from "@follow/components/ui/button/index.js" +import { Input } from "@follow/components/ui/input/index.js" import { authProvidersConfig } from "@follow/constants" import { createSession, loginHandler, signOut } from "@follow/shared/auth" import { DEEPLINK_SCHEME } from "@follow/shared/constants" @@ -30,11 +32,11 @@ function Login() { const { t } = useTranslation("external") useEffect(() => { - if (provider && status === "unauthenticated") { + if (provider && provider !== "credential" && status === "unauthenticated") { loginHandler(provider) setRedirecting(true) } - }, [status]) + }, [provider, status]) const getCallbackUrl = useCallback(async () => { const { data } = await createSession() @@ -59,6 +61,9 @@ function Login() { onceRef.current = true }, [handleOpenApp, isAuthenticated]) + const [email, setEmail] = useState("") + const [password, setPassword] = useState("") + return (
@@ -111,21 +116,49 @@ function Login() {
) : ( <> - {Object.entries(authProviders || []).map(([key, provider]) => ( - - ))} + setEmail(e.target.value)} + /> + setPassword(e.target.value)} + /> + {[ + [ + "credential", + { + id: "credential", + name: "Password", + }, + ] as [string, { id: string; name: string }], + ] + .concat(Object.entries(authProviders || []) as any) + .map(([key, provider]) => ( + + ))} )} diff --git a/locales/errors/en.json b/locales/errors/en.json index 5ed38a44e0..5028d5d73b 100644 --- a/locales/errors/en.json +++ b/locales/errors/en.json @@ -35,6 +35,7 @@ "5002": "Invitation code already used.", "5003": "Invitation code does not exist.", "6000": "User not found", + "6001": "User already has password", "7000": "Setting not found", "7001": "Invalid setting tab", "7002": "Invalid setting payload", diff --git a/locales/external/en.json b/locales/external/en.json index f4fbd5c48c..1d0b10f3f2 100644 --- a/locales/external/en.json +++ b/locales/external/en.json @@ -33,8 +33,10 @@ "invitation.title": "Invitation Code", "login.backToWebApp": "Back To Web App", "login.continueWith": "Continue with {{provider}}", + "login.email": "Email", "login.logInTo": "Log in to ", "login.openApp": "Open App", + "login.password": "Password", "login.redirecting": "Redirecting", "login.signOut": "Sign out", "login.welcomeTo": "Welcome to ", diff --git a/locales/settings/en.json b/locales/settings/en.json index 7bff7b3d06..b59b1fddad 100644 --- a/locales/settings/en.json +++ b/locales/settings/en.json @@ -258,14 +258,17 @@ "lists.title": "Title", "lists.view": "View", "profile.avatar.label": "Avatar", + "profile.current_password.label": "Current Password", "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.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/constants/src/auth-providers.ts b/packages/constants/src/auth-providers.ts index 958bc8fafc..2330f90473 100644 --- a/packages/constants/src/auth-providers.ts +++ b/packages/constants/src/auth-providers.ts @@ -13,6 +13,11 @@ export const authProvidersConfig = { "bg-gray-800 hover:!bg-gray-800/90 focus:!border-gray-800/80 focus:!ring-gray-800/80", iconClassName: "i-mgc-apple-cute-fi", }, + credential: { + buttonClassName: + "bg-neutral-800 hover:!bg-neutral-800/90 focus:!border-neutral-800/80 focus:!ring-neutral-800/80", + iconClassName: "i-mgc-user-3-cute-re", + }, } as Record< string, { diff --git a/packages/shared/src/auth.ts b/packages/shared/src/auth.ts index f3a992c5a4..b10dffd6f6 100644 --- a/packages/shared/src/auth.ts +++ b/packages/shared/src/auth.ts @@ -21,14 +21,36 @@ const authClient = createAuthClient({ plugins: serverPlugins, }) -export const { signIn, signOut, getSession, getProviders, createSession } = authClient +export const { + signIn, + signOut, + getSession, + getProviders, + createSession, + linkSocial, + listAccounts, + changePassword, +} = 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, + args?: { + runtime?: LoginRuntime + email?: string + password?: string + }, +) => { + const { runtime = "app", email, password } = args ?? {} if (IN_ELECTRON) { window.open(`${WEB_URL}/login?provider=${provider}`) } else { + if (provider === "credential") { + if (!email || !password) 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 a8ef01ca3e..8f143e2147 100644 --- a/packages/shared/src/hono.ts +++ b/packages/shared/src/hono.ts @@ -24,12 +24,7 @@ declare const authPlugins: ({ method: "GET"; }> | undefined)?]>(...ctx: C): Promise; + }] ? Response : any>; path: "/get-providers"; options: { method: "GET"; @@ -58,6 +53,23 @@ declare const authPlugins: ({ headers: Headers; }; }; +} | { + id: "updateUserccc"; + endpoints: { + updateUserccc: { + | undefined)?]>(...ctx: C): Promise; + path: "/update-user-ccc"; + options: { + method: "POST"; + }; + method: better_call.Method | better_call.Method[]; + headers: Headers; + }; + }; })[]; declare const achievements: drizzle_orm_pg_core.PgTableWithColumns<{ @@ -4735,10 +4747,10 @@ declare const user: drizzle_orm_pg_core.PgTableWithColumns<{ emailVerified: drizzle_orm_pg_core.PgColumn<{ name: "emailVerified"; tableName: "user"; - dataType: "date"; - columnType: "PgTimestamp"; - data: Date; - driverParam: string; + dataType: "boolean"; + columnType: "PgBoolean"; + data: boolean; + driverParam: boolean; notNull: false; hasDefault: false; isPrimaryKey: false; @@ -4878,10 +4890,10 @@ declare const users: drizzle_orm_pg_core.PgTableWithColumns<{ emailVerified: drizzle_orm_pg_core.PgColumn<{ name: "emailVerified"; tableName: "user"; - dataType: "date"; - columnType: "PgTimestamp"; - data: Date; - driverParam: string; + dataType: "boolean"; + columnType: "PgBoolean"; + data: boolean; + driverParam: boolean; notNull: false; hasDefault: false; isPrimaryKey: false; @@ -4968,7 +4980,7 @@ declare const usersOpenApiSchema: z.ZodObject; email: z.ZodString; - emailVerified: z.ZodNullable; + emailVerified: z.ZodNullable; image: z.ZodNullable; handle: z.ZodNullable; createdAt: z.ZodDate; @@ -4976,7 +4988,7 @@ declare const usersOpenApiSchema: z.ZodObject, z.UnknownKeysParam, z.ZodTypeAny, { name: string | null; id: string; - emailVerified: string | null; + emailVerified: boolean | null; image: string | null; handle: string | null; createdAt: Date; @@ -4984,7 +4996,7 @@ declare const usersOpenApiSchema: z.ZodObject; + disableRefresh: zod.ZodOptional; }, "strip", zod.ZodTypeAny, { disableCookieCache?: boolean | undefined; + disableRefresh?: boolean | undefined; }, { disableCookieCache?: boolean | undefined; + disableRefresh?: boolean | undefined; }>>; requireHeaders: true; metadata: { @@ -6599,12 +6614,7 @@ declare const auth: { method: "GET"; }> | undefined)?]>(...ctx: C): Promise; + }] ? Response : any>; path: "/get-providers"; options: { method: "GET"; @@ -6628,6 +6638,19 @@ declare const auth: { method: better_call.Method | better_call.Method[]; headers: Headers; }; + updateUserccc: { + | undefined)?]>(...ctx: C): Promise; + path: "/update-user-ccc"; + options: { + method: "POST"; + }; + method: better_call.Method | better_call.Method[]; + headers: Headers; + }; signInSocial: { ]>(...ctx: C): Promise; @@ -7989,20 +8034,20 @@ declare const auth: { ; - image: zod.ZodOptional; - }, zod.UnknownKeysParam, zod.ZodTypeAny, { - name?: string | undefined; - image?: string | undefined; - }, { - name?: string | undefined; - image?: string | undefined; - }> & zod.ZodObject<{ handle: zod.ZodString; }, zod.UnknownKeysParam, zod.ZodTypeAny, { handle: string; }, { handle: string; + }> & zod.ZodObject<{ + name: zod.ZodOptional; + image: zod.ZodOptional; + }, zod.UnknownKeysParam, zod.ZodTypeAny, { + name?: string | undefined; + image?: string | null | undefined; + }, { + name?: string | undefined; + image?: string | null | undefined; }>; use: better_call.Endpoint; - image: zod.ZodOptional; - }, zod.UnknownKeysParam, zod.ZodTypeAny, { - name?: string | undefined; - image?: string | undefined; - }, { - name?: string | undefined; - image?: string | undefined; - }> & zod.ZodObject<{ handle: zod.ZodString; }, zod.UnknownKeysParam, zod.ZodTypeAny, { handle: string; }, { handle: string; + }> & zod.ZodObject<{ + name: zod.ZodOptional; + image: zod.ZodOptional; + }, zod.UnknownKeysParam, zod.ZodTypeAny, { + name?: string | undefined; + image?: string | null | undefined; + }, { + name?: string | undefined; + image?: string | null | undefined; }>; use: better_call.Endpoint; use: better_call.Endpoint & { @@ -8210,19 +8248,15 @@ declare const auth: { }; }; }; - }>]>(...ctx: C): Promise | undefined)?]>(...ctx: C): Promise; + }] ? Response : { + success: boolean; + message: string; + }>; path: "/delete-user"; options: { method: "POST"; - body: zod.ZodObject<{ - password: zod.ZodString; - }, "strip", zod.ZodTypeAny, { - password: string; - }, { - password: string; - }>; use: better_call.Endpoint & { @@ -9071,6 +9105,36 @@ declare const auth: { method: better_call.Method | better_call.Method[]; headers: Headers; }; + deleteUserCallback: { + ; + }>]>(...ctx: C): Promise; + path: "/delete-user/callback"; + options: { + method: "GET"; + query: zod.ZodObject<{ + token: zod.ZodString; + }, "strip", zod.ZodTypeAny, { + token: string; + }, { + token: string; + }>; + }; + method: better_call.Method | better_call.Method[]; + headers: Headers; + }; }; options: { appName: string; @@ -9116,7 +9180,6 @@ declare const auth: { }): Promise; options: better_auth_adapters_drizzle.DrizzleAdapterConfig; }; - baseUrl: string | undefined; basePath: string; trustedOrigins: string[]; user: { @@ -9126,25 +9189,32 @@ declare const auth: { }; }; }; - accounts: { + account: { accountLinking: { - enabled: boolean; - trustedProviders: string[]; + enabled: true; + trustedProviders: ("github" | "apple" | "google")[]; }; }; socialProviders: { google: { clientId: string; clientSecret: string; + redirectURI: string; }; github: { clientId: string; clientSecret: string; + redirectURI: string; }; apple: { + enabled: boolean; clientId: string; clientSecret: string; - } | undefined; + redirectURI: string; + }; + }; + emailAndPassword: { + enabled: true; }; plugins: ({ id: "custom-session"; @@ -9207,12 +9277,7 @@ declare const auth: { method: "GET"; }> | undefined)?]>(...ctx: C): Promise; + }] ? Response : any>; path: "/get-providers"; options: { method: "GET"; @@ -9241,6 +9306,23 @@ declare const auth: { headers: Headers; }; }; + } | { + id: "updateUserccc"; + endpoints: { + updateUserccc: { + | undefined)?]>(...ctx: C): Promise; + path: "/update-user-ccc"; + options: { + method: "POST"; + }; + method: better_call.Method | better_call.Method[]; + headers: Headers; + }; + }; })[]; }; $context: Promise; @@ -9569,6 +9651,21 @@ declare const _routes: hono_hono_base.HonoBase | hono_types.MergeSchemaPath<{ "/": { $get: { @@ -9726,7 +9823,7 @@ declare const _routes: hono_hono_base.HonoBase Date: Mon, 9 Dec 2024 14:33:54 +0000 Subject: [PATCH 02/36] chore: auto-fix linting and formatting issues --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 49e2a57be2..9b416258ab 100644 --- a/package.json +++ b/package.json @@ -135,10 +135,10 @@ "@tanstack/react-virtual": "patches/@tanstack__react-virtual.patch" }, "overrides": { - "is-core-module": "npm:@nolyfill/is-core-module@^1", - "isarray": "npm:@nolyfill/isarray@^1", "@types/react": "npm:@types/react@^18.3.12", - "@types/react-dom": "npm:@types/react-dom@^18.3.1" + "@types/react-dom": "npm:@types/react-dom@^18.3.1", + "is-core-module": "npm:@nolyfill/is-core-module@^1", + "isarray": "npm:@nolyfill/isarray@^1" } }, "simple-git-hooks": { @@ -176,4 +176,4 @@ }, "productName": "Follow", "mainHash": "a17b3455d95c59da9a9724da5c9a04332f454caf2391c53ac32fc54184965658" -} \ No newline at end of file +} From c56d788459ef2f214ad48399639567155698c34d Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Mon, 9 Dec 2024 22:40:06 +0800 Subject: [PATCH 03/36] feat: show email --- apps/renderer/src/modules/profile/profile-setting-form.tsx | 5 +++++ locales/settings/en.json | 1 + 2 files changed, 6 insertions(+) diff --git a/apps/renderer/src/modules/profile/profile-setting-form.tsx b/apps/renderer/src/modules/profile/profile-setting-form.tsx index 1319b59303..4bc0038f13 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 { cn } from "@follow/utils/utils" import { zodResolver } from "@hookform/resolvers/zod" import { useMutation } from "@tanstack/react-query" @@ -72,6 +73,10 @@ export const ProfileSettingForm = ({ return (
+
+ +

{user?.email}

+
Date: Tue, 10 Dec 2024 11:38:44 +0800 Subject: [PATCH 04/36] refactor: login form --- apps/server/client/pages/(login)/login.tsx | 130 +++++++++++++-------- locales/external/en.json | 2 + packages/constants/src/auth-providers.ts | 5 - 3 files changed, 86 insertions(+), 51 deletions(-) diff --git a/apps/server/client/pages/(login)/login.tsx b/apps/server/client/pages/(login)/login.tsx index 6609670c61..3e80f539b7 100644 --- a/apps/server/client/pages/(login)/login.tsx +++ b/apps/server/client/pages/(login)/login.tsx @@ -4,19 +4,87 @@ 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 { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from "@follow/components/ui/form/index.jsx" import { Input } from "@follow/components/ui/input/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 { zodResolver } from "@hookform/resolvers/zod" import { useCallback, useEffect, useRef, useState } from "react" +import { useForm } from "react-hook-form" import { useTranslation } from "react-i18next" import { useLocation } from "react-router" +import { z } from "zod" export function Component() { return } +const formSchema = z.object({ + email: z.string().email(), + password: z.string().min(8).max(128), +}) + +async function onSubmit(values: z.infer) { + await loginHandler("credential", values) + queryClient.invalidateQueries({ queryKey: ["auth", "session"] }) +} + +function LoginWithPassword() { + const { t } = useTranslation("external") + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + email: "", + password: "", + }, + }) + + return ( +
+

{t("login.or")}

+ + + ( + + + + + + + )} + /> + ( + + + + + + + )} + /> + + + +
+ ) +} + function Login() { const { status, refetch } = useSession() @@ -61,9 +129,6 @@ function Login() { onceRef.current = true }, [handleOpenApp, isAuthenticated]) - const [email, setEmail] = useState("") - const [password, setPassword] = useState("") - return (
@@ -116,49 +181,22 @@ function Login() {
) : ( <> - setEmail(e.target.value)} - /> - setPassword(e.target.value)} - /> - {[ - [ - "credential", - { - id: "credential", - name: "Password", - }, - ] as [string, { id: string; name: string }], - ] - .concat(Object.entries(authProviders || []) as any) - .map(([key, provider]) => ( - - ))} + {Object.entries(authProviders || []).map(([key, provider]) => ( + + ))} + )} diff --git a/locales/external/en.json b/locales/external/en.json index 1d0b10f3f2..e35987eb0e 100644 --- a/locales/external/en.json +++ b/locales/external/en.json @@ -34,8 +34,10 @@ "login.backToWebApp": "Back To Web App", "login.continueWith": "Continue with {{provider}}", "login.email": "Email", + "login.logIn": "Log in", "login.logInTo": "Log in to ", "login.openApp": "Open App", + "login.or": "Or", "login.password": "Password", "login.redirecting": "Redirecting", "login.signOut": "Sign out", diff --git a/packages/constants/src/auth-providers.ts b/packages/constants/src/auth-providers.ts index 2330f90473..958bc8fafc 100644 --- a/packages/constants/src/auth-providers.ts +++ b/packages/constants/src/auth-providers.ts @@ -13,11 +13,6 @@ export const authProvidersConfig = { "bg-gray-800 hover:!bg-gray-800/90 focus:!border-gray-800/80 focus:!ring-gray-800/80", iconClassName: "i-mgc-apple-cute-fi", }, - credential: { - buttonClassName: - "bg-neutral-800 hover:!bg-neutral-800/90 focus:!border-neutral-800/80 focus:!ring-neutral-800/80", - iconClassName: "i-mgc-user-3-cute-re", - }, } as Record< string, { From 37b488be12461e3f112ed62c08cffa444674768a Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Tue, 10 Dec 2024 11:39:59 +0800 Subject: [PATCH 05/36] chore: update --- apps/server/client/pages/(login)/login.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/client/pages/(login)/login.tsx b/apps/server/client/pages/(login)/login.tsx index 3e80f539b7..066c063531 100644 --- a/apps/server/client/pages/(login)/login.tsx +++ b/apps/server/client/pages/(login)/login.tsx @@ -185,7 +185,7 @@ function Login() { - - - - ) -} - function Login() { const { status, refetch } = useSession() @@ -209,3 +147,65 @@ function Login() { ) } + +const formSchema = z.object({ + email: z.string().email(), + password: z.string().min(8).max(128), +}) + +async function onSubmit(values: z.infer) { + const res = await loginHandler("credential", 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: "", + }, + }) + + return ( +
+

{t("login.or")}

+
+ + ( + + + + + + + )} + /> + ( + + + + + + + )} + /> + + + +
+ ) +} From 5fb28e85c7f5b1db1a39ae290476c44e3ec407f8 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Tue, 10 Dec 2024 16:34:05 +0800 Subject: [PATCH 08/36] credential provider --- apps/server/client/pages/(login)/login.tsx | 110 +++++++++++---------- 1 file changed, 59 insertions(+), 51 deletions(-) diff --git a/apps/server/client/pages/(login)/login.tsx b/apps/server/client/pages/(login)/login.tsx index 16d2d4908f..f20362b2ba 100644 --- a/apps/server/client/pages/(login)/login.tsx +++ b/apps/server/client/pages/(login)/login.tsx @@ -37,17 +37,18 @@ function Login() { 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 && provider !== "credential" && status === "unauthenticated") { + if (provider && !isCredentialProvider && status === "unauthenticated") { loginHandler(provider) setRedirecting(true) } - }, [provider, status]) + }, [isCredentialProvider, provider, status]) const getCallbackUrl = useCallback(async () => { const { data } = await createSession() @@ -124,22 +125,32 @@ function Login() { ) : ( <> - {Object.entries(authProviders || []).map(([key, provider]) => ( - + ))} + {!!authProviders?.credential && ( +
+ {!isCredentialProvider && ( +

{t("login.or")}

)} - onClick={() => { - loginHandler(key) - }} - > - {" "} - {t("login.continueWith", { provider: provider.name })} - - ))} - + +
+ )} )} @@ -173,39 +184,36 @@ function LoginWithPassword() { }) return ( -
-

{t("login.or")}

-
- - ( - - - - - - - )} - /> - ( - - - - - - - )} - /> - - - -
+
+ + ( + + + + + + + )} + /> + ( + + + + + + + )} + /> + + + ) } From dab66fe687b64aa03dece738b61d14e5e4eec87b Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Tue, 10 Dec 2024 16:53:00 +0800 Subject: [PATCH 09/36] feat: confirm password --- .../modules/profile/update-password-form.tsx | 85 +++++++++++++++---- locales/settings/en.json | 2 + 2 files changed, 69 insertions(+), 18 deletions(-) diff --git a/apps/renderer/src/modules/profile/update-password-form.tsx b/apps/renderer/src/modules/profile/update-password-form.tsx index a0693fb543..4f6c25a309 100644 --- a/apps/renderer/src/modules/profile/update-password-form.tsx +++ b/apps/renderer/src/modules/profile/update-password-form.tsx @@ -22,9 +22,15 @@ import { Queries } from "~/queries" import { useHasPassword } from "~/queries/auth" const passwordSchema = z.string().min(8).max(128) -const initPasswordFormSchema = z.object({ - password: passwordSchema, -}) +const initPasswordFormSchema = z + .object({ + newPassword: passwordSchema, + confirmPassword: passwordSchema, + }) + .refine((data) => data.newPassword === data.confirmPassword, { + message: "Passwords don't match", + path: ["confirmPassword"], + }) const InitPasswordForm = () => { const { t } = useTranslation("settings") @@ -32,14 +38,17 @@ const InitPasswordForm = () => { const form = useForm>({ resolver: zodResolver(initPasswordFormSchema), defaultValues: { - password: "", + newPassword: "", + confirmPassword: "", }, }) const updateMutation = useMutation({ mutationFn: async (values: z.infer) => { await apiClient["auth-app"]["init-password"].$patch({ - json: values, + json: { + password: values.newPassword, + }, }) await Queries.auth.getAccounts().invalidate() }, @@ -63,12 +72,28 @@ const InitPasswordForm = () => {
( + + {t("profile.change_password.label")} + + + + + + )} + /> + ( - {t("profile.new_password.label")} - + @@ -85,10 +110,16 @@ const InitPasswordForm = () => { ) } -const updatePasswordFormSchema = z.object({ - currentPassword: passwordSchema, - newPassword: passwordSchema, -}) +const updatePasswordFormSchema = z + .object({ + currentPassword: passwordSchema, + newPassword: passwordSchema, + confirmPassword: passwordSchema, + }) + .refine((data) => data.newPassword === data.confirmPassword, { + message: "Passwords don't match", + path: ["confirmPassword"], + }) const UpdateExistingPasswordForm = () => { const { t } = useTranslation("settings") @@ -98,6 +129,7 @@ const UpdateExistingPasswordForm = () => { defaultValues: { currentPassword: "", newPassword: "", + confirmPassword: "", }, }) @@ -131,11 +163,14 @@ const UpdateExistingPasswordForm = () => { name="currentPassword" render={({ field }) => ( - {t("profile.current_password.label")} + {t("profile.change_password.label")} - + - )} @@ -145,11 +180,25 @@ const UpdateExistingPasswordForm = () => { name="newPassword" render={({ field }) => ( - {t("profile.new_password.label")} - + + + + + )} + /> + ( + + + - )} diff --git a/locales/settings/en.json b/locales/settings/en.json index 14036ed5cb..fe150cc2c1 100644 --- a/locales/settings/en.json +++ b/locales/settings/en.json @@ -258,6 +258,8 @@ "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.", From 07275368fc48d3f720f9569b0bdf1b8a4d3bf57d Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:17:40 +0800 Subject: [PATCH 10/36] chore: update --- .../modules/profile/update-password-form.tsx | 6 ++-- packages/shared/src/hono.ts | 33 ++++++++++--------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/apps/renderer/src/modules/profile/update-password-form.tsx b/apps/renderer/src/modules/profile/update-password-form.tsx index 4f6c25a309..df9c44f4af 100644 --- a/apps/renderer/src/modules/profile/update-password-form.tsx +++ b/apps/renderer/src/modules/profile/update-password-form.tsx @@ -45,10 +45,8 @@ const InitPasswordForm = () => { const updateMutation = useMutation({ mutationFn: async (values: z.infer) => { - await apiClient["auth-app"]["init-password"].$patch({ - json: { - password: values.newPassword, - }, + await apiClient.profiles["init-password"].$patch({ + json: { password: values.newPassword }, }) await Queries.auth.getAccounts().invalidate() }, diff --git a/packages/shared/src/hono.ts b/packages/shared/src/hono.ts index 8f143e2147..2488b51219 100644 --- a/packages/shared/src/hono.ts +++ b/packages/shared/src/hono.ts @@ -9353,6 +9353,7 @@ declare const auth: { }; type AuthSession = Awaited>; +type AuthUser = NonNullable["user"]; declare const _routes: hono_hono_base.HonoBase | hono_types.MergeSchemaPath<{ "/": { $get: { @@ -10783,6 +10769,21 @@ declare const _routes: hono_hono_base.HonoBase | hono_types.MergeSchemaPath<{ "/": { $post: { @@ -12168,4 +12169,4 @@ declare const _routes: hono_hono_base.HonoBase, "/">; type AppType = typeof _routes; -export { type ActionsModel, type AirdropActivity, type AppType, type AttachmentsModel, type AuthSession, CommonEntryFields, type ConditionItem, type DetailModel, type EntriesModel, type EntryReadHistoriesModel, type ExtraModel, type FeedModel, type MediaModel, type MessagingData, MessagingType, type SettingsModel, account, accountAuthjs, achievements, achievementsOpenAPISchema, actions, actionsItemOpenAPISchema, actionsOpenAPISchema, actionsRelations, activityEnum, airdrops, airdropsOpenAPISchema, attachmentsZodSchema, authPlugins, boosts, collections, collectionsOpenAPISchema, collectionsRelations, detailModelSchema, entries, entriesOpenAPISchema, entriesRelations, entryReadHistories, entryReadHistoriesOpenAPISchema, entryReadHistoriesRelations, extraZodSchema, feedPowerTokens, feedPowerTokensOpenAPISchema, feedPowerTokensRelations, feeds, feedsOpenAPISchema, feedsRelations, inboxHandleSchema, inboxes, inboxesEntries, inboxesEntriesInsertOpenAPISchema, type inboxesEntriesModel, inboxesEntriesOpenAPISchema, inboxesEntriesRelations, inboxesOpenAPISchema, inboxesRelations, invitations, invitationsOpenAPISchema, invitationsRelations, languageSchema, levels, levelsOpenAPISchema, levelsRelations, lists, listsOpenAPISchema, listsRelations, listsSubscriptions, listsSubscriptionsOpenAPISchema, listsSubscriptionsRelations, listsTimeline, listsTimelineOpenAPISchema, listsTimelineRelations, lower, mediaZodSchema, messaging, messagingOpenAPISchema, messagingRelations, session, sessionAuthjs, settings, subscriptions, subscriptionsOpenAPISchema, subscriptionsRelations, timeline, timelineOpenAPISchema, timelineRelations, transactionType, transactions, transactionsOpenAPISchema, transactionsRelations, user, users, usersOpenApiSchema, usersRelations, verification, wallets, walletsOpenAPISchema, walletsRelations }; +export { type ActionsModel, type AirdropActivity, type AppType, type AttachmentsModel, type AuthSession, type AuthUser, CommonEntryFields, type ConditionItem, type DetailModel, type EntriesModel, type EntryReadHistoriesModel, type ExtraModel, type FeedModel, type MediaModel, type MessagingData, MessagingType, type SettingsModel, account, accountAuthjs, achievements, achievementsOpenAPISchema, actions, actionsItemOpenAPISchema, actionsOpenAPISchema, actionsRelations, activityEnum, airdrops, airdropsOpenAPISchema, attachmentsZodSchema, authPlugins, boosts, collections, collectionsOpenAPISchema, collectionsRelations, detailModelSchema, entries, entriesOpenAPISchema, entriesRelations, entryReadHistories, entryReadHistoriesOpenAPISchema, entryReadHistoriesRelations, extraZodSchema, feedPowerTokens, feedPowerTokensOpenAPISchema, feedPowerTokensRelations, feeds, feedsOpenAPISchema, feedsRelations, inboxHandleSchema, inboxes, inboxesEntries, inboxesEntriesInsertOpenAPISchema, type inboxesEntriesModel, inboxesEntriesOpenAPISchema, inboxesEntriesRelations, inboxesOpenAPISchema, inboxesRelations, invitations, invitationsOpenAPISchema, invitationsRelations, languageSchema, levels, levelsOpenAPISchema, levelsRelations, lists, listsOpenAPISchema, listsRelations, listsSubscriptions, listsSubscriptionsOpenAPISchema, listsSubscriptionsRelations, listsTimeline, listsTimelineOpenAPISchema, listsTimelineRelations, lower, mediaZodSchema, messaging, messagingOpenAPISchema, messagingRelations, session, sessionAuthjs, settings, subscriptions, subscriptionsOpenAPISchema, subscriptionsRelations, timeline, timelineOpenAPISchema, timelineRelations, transactionType, transactions, transactionsOpenAPISchema, transactionsRelations, user, users, usersOpenApiSchema, usersRelations, verification, wallets, walletsOpenAPISchema, walletsRelations }; From 5315cb30c3b53cb72a714e992cb69aa72f274029 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:43:05 +0800 Subject: [PATCH 11/36] revokeOtherSessions when update password --- .../modules/profile/update-password-form.tsx | 6 +- packages/shared/src/hono.ts | 488 +++--------------- 2 files changed, 74 insertions(+), 420 deletions(-) diff --git a/apps/renderer/src/modules/profile/update-password-form.tsx b/apps/renderer/src/modules/profile/update-password-form.tsx index df9c44f4af..4222c2c216 100644 --- a/apps/renderer/src/modules/profile/update-password-form.tsx +++ b/apps/renderer/src/modules/profile/update-password-form.tsx @@ -133,7 +133,11 @@ const UpdateExistingPasswordForm = () => { const updateMutation = useMutation({ mutationFn: async (values: z.infer) => { - const res = await changePassword(values) + const res = await changePassword({ + currentPassword: values.currentPassword, + newPassword: values.newPassword, + revokeOtherSessions: true, + }) if (res.error) { throw new Error(res.error.message) } diff --git a/packages/shared/src/hono.ts b/packages/shared/src/hono.ts index 2488b51219..b68f166a9e 100644 --- a/packages/shared/src/hono.ts +++ b/packages/shared/src/hono.ts @@ -2679,6 +2679,23 @@ declare const subscriptions: drizzle_orm_pg_core.PgTableWithColumns<{ identity: undefined; generated: undefined; }, {}, {}>; + createdAt: drizzle_orm_pg_core.PgColumn<{ + name: "created_at"; + tableName: "subscriptions"; + dataType: "date"; + columnType: "PgTimestamp"; + data: Date; + driverParam: string; + notNull: true; + hasDefault: true; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: undefined; + baseColumn: never; + identity: undefined; + generated: undefined; + }, {}, {}>; isPrivate: drizzle_orm_pg_core.PgColumn<{ name: "is_private"; tableName: "subscriptions"; @@ -2705,9 +2722,11 @@ declare const subscriptionsOpenAPISchema: zod.ZodObject<{ view: zod.ZodNumber; category: zod.ZodNullable; title: zod.ZodNullable; + createdAt: zod.ZodString; isPrivate: zod.ZodBoolean; }, zod.UnknownKeysParam, zod.ZodTypeAny, { title: string | null; + createdAt: string; userId: string; view: number; category: string | null; @@ -2715,6 +2734,7 @@ declare const subscriptionsOpenAPISchema: zod.ZodObject<{ isPrivate: boolean; }, { title: string | null; + createdAt: string; userId: string; view: number; category: string | null; @@ -3970,6 +3990,23 @@ declare const invitations: drizzle_orm_pg_core.PgTableWithColumns<{ identity: undefined; generated: undefined; }, {}, {}>; + usedAt: drizzle_orm_pg_core.PgColumn<{ + name: "used_at"; + tableName: "invitations"; + dataType: "date"; + columnType: "PgTimestamp"; + data: Date; + driverParam: string; + notNull: false; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: undefined; + baseColumn: never; + identity: undefined; + generated: undefined; + }, {}, {}>; fromUserId: drizzle_orm_pg_core.PgColumn<{ name: "from_user_id"; tableName: "invitations"; @@ -4010,16 +4047,19 @@ declare const invitations: drizzle_orm_pg_core.PgTableWithColumns<{ declare const invitationsOpenAPISchema: zod.ZodObject<{ code: zod.ZodString; createdAt: zod.ZodNullable; + usedAt: zod.ZodNullable; fromUserId: zod.ZodString; toUserId: zod.ZodNullable; }, zod.UnknownKeysParam, zod.ZodTypeAny, { code: string; createdAt: string | null; + usedAt: string | null; fromUserId: string; toUserId: string | null; }, { code: string; createdAt: string | null; + usedAt: string | null; fromUserId: string; toUserId: string | null; }>; @@ -4348,6 +4388,23 @@ declare const listsSubscriptions: drizzle_orm_pg_core.PgTableWithColumns<{ identity: undefined; generated: undefined; }, {}, {}>; + createdAt: drizzle_orm_pg_core.PgColumn<{ + name: "created_at"; + tableName: "lists_subscriptions"; + dataType: "date"; + columnType: "PgTimestamp"; + data: Date; + driverParam: string; + notNull: true; + hasDefault: true; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: undefined; + baseColumn: never; + identity: undefined; + generated: undefined; + }, {}, {}>; isPrivate: drizzle_orm_pg_core.PgColumn<{ name: "is_private"; tableName: "lists_subscriptions"; @@ -4374,9 +4431,11 @@ declare const listsSubscriptionsOpenAPISchema: zod.ZodObject<{ view: zod.ZodNumber; title: zod.ZodNullable; lastViewedAt: zod.ZodNullable; + createdAt: zod.ZodString; isPrivate: zod.ZodBoolean; }, zod.UnknownKeysParam, zod.ZodTypeAny, { title: string | null; + createdAt: string; userId: string; view: number; isPrivate: boolean; @@ -4384,6 +4443,7 @@ declare const listsSubscriptionsOpenAPISchema: zod.ZodObject<{ lastViewedAt: string | null; }, { title: string | null; + createdAt: string; userId: string; view: number; isPrivate: boolean; @@ -5230,234 +5290,6 @@ declare const account: drizzle_orm_pg_core.PgTableWithColumns<{ }; dialect: "pg"; }>; -declare const accountAuthjs: drizzle_orm_pg_core.PgTableWithColumns<{ - name: "account"; - schema: undefined; - columns: { - id: drizzle_orm_pg_core.PgColumn<{ - name: "id"; - tableName: "account"; - dataType: "string"; - columnType: "PgText"; - data: string; - driverParam: string; - notNull: true; - hasDefault: true; - isPrimaryKey: true; - isAutoincrement: false; - hasRuntimeDefault: true; - enumValues: [string, ...string[]]; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - userId: drizzle_orm_pg_core.PgColumn<{ - name: "userId"; - tableName: "account"; - dataType: "string"; - columnType: "PgText"; - data: string; - driverParam: string; - notNull: true; - hasDefault: false; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: [string, ...string[]]; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - refreshTokenExpiresAt: drizzle_orm_pg_core.PgColumn<{ - name: "refreshTokenExpiresAt"; - tableName: "account"; - dataType: "date"; - columnType: "PgTimestamp"; - data: Date; - driverParam: string; - notNull: false; - hasDefault: false; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: undefined; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - scope: drizzle_orm_pg_core.PgColumn<{ - name: "scope"; - tableName: "account"; - dataType: "string"; - columnType: "PgText"; - data: string; - driverParam: string; - notNull: false; - hasDefault: false; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: [string, ...string[]]; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - password: drizzle_orm_pg_core.PgColumn<{ - name: "password"; - tableName: "account"; - dataType: "string"; - columnType: "PgText"; - data: string; - driverParam: string; - notNull: false; - hasDefault: false; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: [string, ...string[]]; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - createdAt: drizzle_orm_pg_core.PgColumn<{ - name: "createdAt"; - tableName: "account"; - dataType: "date"; - columnType: "PgTimestamp"; - data: Date; - driverParam: string; - notNull: true; - hasDefault: true; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: undefined; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - updatedAt: drizzle_orm_pg_core.PgColumn<{ - name: "updatedAt"; - tableName: "account"; - dataType: "date"; - columnType: "PgTimestamp"; - data: Date; - driverParam: string; - notNull: true; - hasDefault: true; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: undefined; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - provider: drizzle_orm_pg_core.PgColumn<{ - name: "provider"; - tableName: "account"; - dataType: "string"; - columnType: "PgText"; - data: string; - driverParam: string; - notNull: true; - hasDefault: false; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: [string, ...string[]]; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - providerAccountId: drizzle_orm_pg_core.PgColumn<{ - name: "providerAccountId"; - tableName: "account"; - dataType: "string"; - columnType: "PgText"; - data: string; - driverParam: string; - notNull: true; - hasDefault: false; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: [string, ...string[]]; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - refresh_token: drizzle_orm_pg_core.PgColumn<{ - name: "refresh_token"; - tableName: "account"; - dataType: "string"; - columnType: "PgText"; - data: string; - driverParam: string; - notNull: false; - hasDefault: false; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: [string, ...string[]]; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - access_token: drizzle_orm_pg_core.PgColumn<{ - name: "access_token"; - tableName: "account"; - dataType: "string"; - columnType: "PgText"; - data: string; - driverParam: string; - notNull: false; - hasDefault: false; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: [string, ...string[]]; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - expires_at: drizzle_orm_pg_core.PgColumn<{ - name: "expires_at"; - tableName: "account"; - dataType: "date"; - columnType: "PgTimestamp"; - data: Date; - driverParam: string; - notNull: false; - hasDefault: false; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: undefined; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - id_token: drizzle_orm_pg_core.PgColumn<{ - name: "id_token"; - tableName: "account"; - dataType: "string"; - columnType: "PgText"; - data: string; - driverParam: string; - notNull: false; - hasDefault: false; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: [string, ...string[]]; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - }; - dialect: "pg"; -}>; declare const session: drizzle_orm_pg_core.PgTableWithColumns<{ name: "session"; schema: undefined; @@ -5601,149 +5433,6 @@ declare const session: drizzle_orm_pg_core.PgTableWithColumns<{ }; dialect: "pg"; }>; -declare const sessionAuthjs: drizzle_orm_pg_core.PgTableWithColumns<{ - name: "session"; - schema: undefined; - columns: { - id: drizzle_orm_pg_core.PgColumn<{ - name: "id"; - tableName: "session"; - dataType: "string"; - columnType: "PgText"; - data: string; - driverParam: string; - notNull: true; - hasDefault: true; - isPrimaryKey: true; - isAutoincrement: false; - hasRuntimeDefault: true; - enumValues: [string, ...string[]]; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - userId: drizzle_orm_pg_core.PgColumn<{ - name: "userId"; - tableName: "session"; - dataType: "string"; - columnType: "PgText"; - data: string; - driverParam: string; - notNull: true; - hasDefault: false; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: [string, ...string[]]; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - createdAt: drizzle_orm_pg_core.PgColumn<{ - name: "createdAt"; - tableName: "session"; - dataType: "date"; - columnType: "PgTimestamp"; - data: Date; - driverParam: string; - notNull: true; - hasDefault: true; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: undefined; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - updatedAt: drizzle_orm_pg_core.PgColumn<{ - name: "updatedAt"; - tableName: "session"; - dataType: "date"; - columnType: "PgTimestamp"; - data: Date; - driverParam: string; - notNull: true; - hasDefault: true; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: undefined; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - ipAddress: drizzle_orm_pg_core.PgColumn<{ - name: "ipAddress"; - tableName: "session"; - dataType: "string"; - columnType: "PgText"; - data: string; - driverParam: string; - notNull: false; - hasDefault: false; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: [string, ...string[]]; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - userAgent: drizzle_orm_pg_core.PgColumn<{ - name: "userAgent"; - tableName: "session"; - dataType: "string"; - columnType: "PgText"; - data: string; - driverParam: string; - notNull: false; - hasDefault: false; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: [string, ...string[]]; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - sessionToken: drizzle_orm_pg_core.PgColumn<{ - name: "sessionToken"; - tableName: "session"; - dataType: "string"; - columnType: "PgText"; - data: string; - driverParam: string; - notNull: false; - hasDefault: false; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: [string, ...string[]]; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - expires: drizzle_orm_pg_core.PgColumn<{ - name: "expires"; - tableName: "session"; - dataType: "date"; - columnType: "PgTimestamp"; - data: Date; - driverParam: string; - notNull: true; - hasDefault: false; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: undefined; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - }; - dialect: "pg"; -}>; declare const verification: drizzle_orm_pg_core.PgTableWithColumns<{ name: "verificationToken"; schema: undefined; @@ -6545,6 +6234,7 @@ declare const auth: { invitation: { code: string; createdAt: Date | null; + usedAt: Date | null; fromUserId: string; toUserId: string | null; } | undefined; @@ -9253,6 +8943,7 @@ declare const auth: { invitation: { code: string; createdAt: Date | null; + usedAt: Date | null; fromUserId: string; toUserId: string | null; } | undefined; @@ -9606,53 +9297,6 @@ declare const _routes: hono_hono_base.HonoBase | hono_types.MergeSchemaPath<{ - "/new-session": { - $post: { - input: {}; - output: { - code: 0; - data: { - userId: string; - sessionToken: string; - expires: string; - }; - }; - outputFormat: "json" | "text"; - status: 200; - }; - }; -} & { - "/update-account": { - $patch: { - input: { - json: { - name?: string | null | undefined; - image?: string | null | undefined; - handle?: string | null | undefined; - }; - }; - output: { - code: 0; - }; - outputFormat: "json" | "text"; - status: 200; - }; - }; -} & { - "/providers": { - $get: { - input: {}; - output: { - [x: string]: { - name: string; - id: string; - }; - }; - outputFormat: "json" | "text"; - status: 200; - }; - }; -}, "/auth-app"> | hono_types.MergeSchemaPath<{ "/": { $get: { input: { @@ -10616,6 +10260,7 @@ declare const _routes: hono_hono_base.HonoBase, "/">; type AppType = typeof _routes; -export { type ActionsModel, type AirdropActivity, type AppType, type AttachmentsModel, type AuthSession, type AuthUser, CommonEntryFields, type ConditionItem, type DetailModel, type EntriesModel, type EntryReadHistoriesModel, type ExtraModel, type FeedModel, type MediaModel, type MessagingData, MessagingType, type SettingsModel, account, accountAuthjs, achievements, achievementsOpenAPISchema, actions, actionsItemOpenAPISchema, actionsOpenAPISchema, actionsRelations, activityEnum, airdrops, airdropsOpenAPISchema, attachmentsZodSchema, authPlugins, boosts, collections, collectionsOpenAPISchema, collectionsRelations, detailModelSchema, entries, entriesOpenAPISchema, entriesRelations, entryReadHistories, entryReadHistoriesOpenAPISchema, entryReadHistoriesRelations, extraZodSchema, feedPowerTokens, feedPowerTokensOpenAPISchema, feedPowerTokensRelations, feeds, feedsOpenAPISchema, feedsRelations, inboxHandleSchema, inboxes, inboxesEntries, inboxesEntriesInsertOpenAPISchema, type inboxesEntriesModel, inboxesEntriesOpenAPISchema, inboxesEntriesRelations, inboxesOpenAPISchema, inboxesRelations, invitations, invitationsOpenAPISchema, invitationsRelations, languageSchema, levels, levelsOpenAPISchema, levelsRelations, lists, listsOpenAPISchema, listsRelations, listsSubscriptions, listsSubscriptionsOpenAPISchema, listsSubscriptionsRelations, listsTimeline, listsTimelineOpenAPISchema, listsTimelineRelations, lower, mediaZodSchema, messaging, messagingOpenAPISchema, messagingRelations, session, sessionAuthjs, settings, subscriptions, subscriptionsOpenAPISchema, subscriptionsRelations, timeline, timelineOpenAPISchema, timelineRelations, transactionType, transactions, transactionsOpenAPISchema, transactionsRelations, user, users, usersOpenApiSchema, usersRelations, verification, wallets, walletsOpenAPISchema, walletsRelations }; +export { type ActionsModel, type AirdropActivity, type AppType, type AttachmentsModel, type AuthSession, type AuthUser, CommonEntryFields, type ConditionItem, type DetailModel, type EntriesModel, type EntryReadHistoriesModel, type ExtraModel, type FeedModel, type MediaModel, type MessagingData, MessagingType, type SettingsModel, account, achievements, achievementsOpenAPISchema, actions, actionsItemOpenAPISchema, actionsOpenAPISchema, actionsRelations, activityEnum, airdrops, airdropsOpenAPISchema, attachmentsZodSchema, authPlugins, boosts, collections, collectionsOpenAPISchema, collectionsRelations, detailModelSchema, entries, entriesOpenAPISchema, entriesRelations, entryReadHistories, entryReadHistoriesOpenAPISchema, entryReadHistoriesRelations, extraZodSchema, feedPowerTokens, feedPowerTokensOpenAPISchema, feedPowerTokensRelations, feeds, feedsOpenAPISchema, feedsRelations, inboxHandleSchema, inboxes, inboxesEntries, inboxesEntriesInsertOpenAPISchema, type inboxesEntriesModel, inboxesEntriesOpenAPISchema, inboxesEntriesRelations, inboxesOpenAPISchema, inboxesRelations, invitations, invitationsOpenAPISchema, invitationsRelations, languageSchema, levels, levelsOpenAPISchema, levelsRelations, lists, listsOpenAPISchema, listsRelations, listsSubscriptions, listsSubscriptionsOpenAPISchema, listsSubscriptionsRelations, listsTimeline, listsTimelineOpenAPISchema, listsTimelineRelations, lower, mediaZodSchema, messaging, messagingOpenAPISchema, messagingRelations, session, settings, subscriptions, subscriptionsOpenAPISchema, subscriptionsRelations, timeline, timelineOpenAPISchema, timelineRelations, transactionType, transactions, transactionsOpenAPISchema, transactionsRelations, user, users, usersOpenApiSchema, usersRelations, verification, wallets, walletsOpenAPISchema, walletsRelations }; From 777b1f3f4f46ef0cd88ab44fa435fd986576c49d Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:44:18 +0800 Subject: [PATCH 12/36] changelog --- changelog/next.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog/next.md b/changelog/next.md index 7acf8eab35..7f63acc85c 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -5,6 +5,7 @@ - New `Move to Category` operation in feed subscription context menu - New `Expand long social media` setting to automatically expand social media entries containing long text. - New `Back Top` button and read progress indicator in entry content +- Login with email and password ## Improvements From 8ea7183b24e841f90e46c554c29f964a62a6812a Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:49:19 +0800 Subject: [PATCH 13/36] typecheck --- apps/renderer/src/modules/user/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/renderer/src/modules/user/utils.ts b/apps/renderer/src/modules/user/utils.ts index 89c74173d1..bfd62187c7 100644 --- a/apps/renderer/src/modules/user/utils.ts +++ b/apps/renderer/src/modules/user/utils.ts @@ -1,7 +1,7 @@ export interface User { name: string | null id: string - emailVerified: string | null + emailVerified: string | null | boolean image: string | null handle: string | null createdAt: string From cf6a2b5ccc1400073a34be702cc3e1e29589816f Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Tue, 10 Dec 2024 19:18:44 +0800 Subject: [PATCH 14/36] chore: update --- locales/errors/en.json | 1 - 1 file changed, 1 deletion(-) diff --git a/locales/errors/en.json b/locales/errors/en.json index 5028d5d73b..5ed38a44e0 100644 --- a/locales/errors/en.json +++ b/locales/errors/en.json @@ -35,7 +35,6 @@ "5002": "Invitation code already used.", "5003": "Invitation code does not exist.", "6000": "User not found", - "6001": "User already has password", "7000": "Setting not found", "7001": "Invalid setting tab", "7002": "Invalid setting payload", From f7b716f5054e3354048d88768b91caf5239ac1d8 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Thu, 12 Dec 2024 16:57:45 +0800 Subject: [PATCH 15/36] chore: update hono --- packages/shared/src/hono.ts | 88 +++---------------------------------- 1 file changed, 5 insertions(+), 83 deletions(-) diff --git a/packages/shared/src/hono.ts b/packages/shared/src/hono.ts index b6198a61cf..4ad5215231 100644 --- a/packages/shared/src/hono.ts +++ b/packages/shared/src/hono.ts @@ -8905,6 +8905,11 @@ declare const auth: { }; emailAndPassword: { enabled: true; + sendResetPassword({ user, url }: { + user: better_auth.User; + url: string; + token: string; + }): Promise; }; plugins: ({ id: "custom-session"; @@ -10415,21 +10420,6 @@ declare const _routes: hono_hono_base.HonoBase | hono_types.MergeSchemaPath<{ "/": { $post: { @@ -11117,74 +11107,6 @@ declare const _routes: hono_hono_base.HonoBase Date: Thu, 12 Dec 2024 17:03:37 +0800 Subject: [PATCH 16/36] feat: forget password --- .../modules/profile/update-password-form.tsx | 108 +++++------------- locales/settings/en.json | 2 + packages/shared/src/auth.ts | 1 + 3 files changed, 33 insertions(+), 78 deletions(-) diff --git a/apps/renderer/src/modules/profile/update-password-form.tsx b/apps/renderer/src/modules/profile/update-password-form.tsx index 4222c2c216..22dc6af6af 100644 --- a/apps/renderer/src/modules/profile/update-password-form.tsx +++ b/apps/renderer/src/modules/profile/update-password-form.tsx @@ -8,7 +8,8 @@ import { FormMessage, } from "@follow/components/ui/form/index.jsx" import { Input } from "@follow/components/ui/input/index.js" -import { changePassword } from "@follow/shared/auth" +import { changePassword, 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" @@ -16,98 +17,49 @@ import { useTranslation } from "react-i18next" import { toast } from "sonner" import { z } from "zod" -import { apiClient } from "~/lib/api-fetch" -import { toastFetchError } from "~/lib/error-parser" -import { Queries } from "~/queries" +import { useWhoami } from "~/atoms/user" import { useHasPassword } from "~/queries/auth" -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"], - }) - -const InitPasswordForm = () => { +const ForgetPasswordButton = () => { const { t } = useTranslation("settings") - - const form = useForm>({ - resolver: zodResolver(initPasswordFormSchema), - defaultValues: { - newPassword: "", - confirmPassword: "", - }, - }) - - const updateMutation = useMutation({ - mutationFn: async (values: z.infer) => { - await apiClient.profiles["init-password"].$patch({ - json: { password: values.newPassword }, + const user = useWhoami() + const forgetPasswordMutation = useMutation({ + mutationFn: async () => { + if (!user) { + throw new Error("No user found") + } + const res = await forgetPassword({ + email: user.email, + redirectTo: `${env.VITE_WEB_URL}/reset-password`, }) - await Queries.auth.getAccounts().invalidate() + if (res.error) { + throw new Error(res.error.message) + } }, onError: (error) => { - toastFetchError(error) + toast.error(error.message) }, - onSuccess: (_) => { - toast(t("profile.update_password_success"), { + onSuccess: () => { + toast(t("profile.reset_password_mail_sent"), { duration: 3000, }) - form.reset() }, }) - function onSubmit(values: z.infer) { - updateMutation.mutate(values) - } - return ( - - - ( - - {t("profile.change_password.label")} - - - - - - )} - /> - ( - - - - - - - )} - /> - -
- -
- - + ) } +const passwordSchema = z.string().min(8).max(128) + const updatePasswordFormSchema = z .object({ currentPassword: passwordSchema, @@ -223,7 +175,7 @@ export const UpdatePasswordForm = () => { } if (!hasPassword) { - return + return } return } diff --git a/locales/settings/en.json b/locales/settings/en.json index a08ff0f1b6..b53e1d1bff 100644 --- a/locales/settings/en.json +++ b/locales/settings/en.json @@ -263,11 +263,13 @@ "profile.confirm_password.label": "Confirm Password", "profile.current_password.label": "Current Password", "profile.email.label": "Email", + "profile.forget_password": "Forget Password", "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", diff --git a/packages/shared/src/auth.ts b/packages/shared/src/auth.ts index b10dffd6f6..9caa7ecec3 100644 --- a/packages/shared/src/auth.ts +++ b/packages/shared/src/auth.ts @@ -30,6 +30,7 @@ export const { linkSocial, listAccounts, changePassword, + forgetPassword, } = authClient export const LOGIN_CALLBACK_URL = `${WEB_URL}/login` From 67bc346831e5b27231ea71319ade2e9ce752c923 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Thu, 12 Dec 2024 22:20:18 +0800 Subject: [PATCH 17/36] chore: update --- packages/shared/src/hono.ts | 68 +++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/packages/shared/src/hono.ts b/packages/shared/src/hono.ts index 4ad5215231..fe95dd0204 100644 --- a/packages/shared/src/hono.ts +++ b/packages/shared/src/hono.ts @@ -11107,6 +11107,74 @@ declare const _routes: hono_hono_base.HonoBase Date: Thu, 12 Dec 2024 22:51:03 +0800 Subject: [PATCH 18/36] feat: reset password page --- .../client/pages/(login)/reset-password.tsx | 120 ++++++++++++++++++ apps/server/package.json | 4 +- locales/external/en.json | 6 + packages/shared/src/auth.ts | 1 + pnpm-lock.yaml | 6 + 5 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 apps/server/client/pages/(login)/reset-password.tsx 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..4e61ee1996 --- /dev/null +++ b/apps/server/client/pages/(login)/reset-password.tsx @@ -0,0 +1,120 @@ +import { Button } 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, + 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 { 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 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")) + }, + }) + + function onSubmit(values: z.infer) { + updateMutation.mutate(values) + } + + return ( +
+ + + {t("login.reset_password.label")} + {t("login.reset_password.description")} + + +
+ + ( + + + + + + + )} + /> + ( + + + + + + + )} + /> + +
+ +
+ + +
+
+
+ ) +} diff --git a/apps/server/package.json b/apps/server/package.json index 70e4bd318c..067decc908 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", @@ -35,7 +36,8 @@ "sonner": "1.7.1", "tailwindcss": "3.4.16", "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/locales/external/en.json b/locales/external/en.json index 03c49cb4e8..291407a727 100644 --- a/locales/external/en.json +++ b/locales/external/en.json @@ -34,15 +34,21 @@ "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.email": "Email", "login.logIn": "Log in", "login.logInTo": "Log in to ", + "login.new_password.label": "New Password", "login.openApp": "Open App", "login.or": "Or", "login.password": "Password", "login.redirecting": "Redirecting", + "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.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.", diff --git a/packages/shared/src/auth.ts b/packages/shared/src/auth.ts index 9caa7ecec3..0d1d0890a0 100644 --- a/packages/shared/src/auth.ts +++ b/packages/shared/src/auth.ts @@ -31,6 +31,7 @@ export const { listAccounts, changePassword, forgetPassword, + resetPassword, } = authClient export const LOGIN_CALLBACK_URL = `${WEB_URL}/login` diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e2fd4496f..dcfd20cca3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -804,6 +804,9 @@ importers: react-blurhash: specifier: ^0.3.0 version: 0.3.0(blurhash@2.0.5)(react@18.3.1) + react-hook-form: + specifier: 7.54.0 + version: 7.54.0(react@18.3.1) react-hotkeys-hook: specifier: 4.6.1 version: 4.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -831,6 +834,9 @@ importers: xss: specifier: 1.0.15 version: 1.0.15 + zod: + specifier: 3.23.8 + version: 3.23.8 devDependencies: '@follow/components': specifier: workspace:* From 05321526d744005f331caab891899b10ba55071b Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:09:06 +0800 Subject: [PATCH 19/36] feat: register form --- apps/server/client/pages/(login)/register.tsx | 128 ++++++++++++++++++ locales/external/en.json | 8 +- packages/shared/src/auth.ts | 17 ++- 3 files changed, 145 insertions(+), 8 deletions(-) create mode 100644 apps/server/client/pages/(login)/register.tsx diff --git a/apps/server/client/pages/(login)/register.tsx b/apps/server/client/pages/(login)/register.tsx new file mode 100644 index 0000000000..3666b5ca08 --- /dev/null +++ b/apps/server/client/pages/(login)/register.tsx @@ -0,0 +1,128 @@ +import { Button } 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 { signUp } from "@follow/shared/auth" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +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), + passwordConfirmation: z.string(), + }) + .refine((data) => data.password === data.passwordConfirmation, { + message: "Passwords do not match", + }) + +function onSubmit(values: z.infer) { + return signUp.email({ + email: values.email, + password: values.password, + name: values.email.split("@")[0], + callbackURL: "/", + fetchOptions: { + onSuccess() { + window.location.href = "/" + }, + onError(context) { + toast.error(context.error.message) + }, + }, + }) +} + +function RegisterForm() { + const { t } = useTranslation("external") + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + email: "", + password: "", + passwordConfirmation: "", + }, + }) + + return ( + + + {t("register.label")} + + {t("register.description")} + + + +
+ + ( + + {t("register.email")} + + + + + + )} + /> + ( + + {t("register.password")} + + + + + + )} + /> + ( + + {t("register.confirm_password")} + + + + + + )} + /> + + + +
+
+ ) +} diff --git a/locales/external/en.json b/locales/external/en.json index 291407a727..af6235de9f 100644 --- a/locales/external/en.json +++ b/locales/external/en.json @@ -53,5 +53,11 @@ "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.description": "Enter your email and password to create an account", + "register.email": "Email", + "register.label": "Register", + "register.password": "Password", + "register.submit": "Submit" } diff --git a/packages/shared/src/auth.ts b/packages/shared/src/auth.ts index 5091293939..fdfade2d85 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,18 +30,20 @@ const authClient = createAuthClient({ plugins: serverPlugins, }) +// @keep-sorted export const { - signIn, - signOut, - getSession, - getProviders, + changePassword, createSession, - updateUser, + forgetPassword, + getProviders, + getSession, linkSocial, listAccounts, - changePassword, - forgetPassword, resetPassword, + signIn, + signOut, + signUp, + updateUser, } = authClient export const LOGIN_CALLBACK_URL = `${WEB_URL}/login` From 079478bb46416d7f455e7ee1f5240422774c8d56 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:12:17 +0800 Subject: [PATCH 20/36] chore: update --- apps/server/client/pages/(login)/register.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/server/client/pages/(login)/register.tsx b/apps/server/client/pages/(login)/register.tsx index 3666b5ca08..258d3ad0c8 100644 --- a/apps/server/client/pages/(login)/register.tsx +++ b/apps/server/client/pages/(login)/register.tsx @@ -1,3 +1,4 @@ +import { Logo } from "@follow/components/icons/logo.jsx" import { Button } from "@follow/components/ui/button/index.jsx" import { Card, @@ -24,7 +25,8 @@ import { z } from "zod" export function Component() { return ( -
+
+
) @@ -72,10 +74,8 @@ function RegisterForm() { return ( - {t("register.label")} - - {t("register.description")} - + {t("register.label")} + {t("register.description")}
From f77e4817b06aa8845fcf709a3d1234e97f5915ae Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:26:45 +0800 Subject: [PATCH 21/36] chore: update --- apps/server/client/pages/(login)/register.tsx | 143 ++++++++++-------- locales/external/en.json | 7 +- 2 files changed, 81 insertions(+), 69 deletions(-) diff --git a/apps/server/client/pages/(login)/register.tsx b/apps/server/client/pages/(login)/register.tsx index 258d3ad0c8..3fb2f7606c 100644 --- a/apps/server/client/pages/(login)/register.tsx +++ b/apps/server/client/pages/(login)/register.tsx @@ -1,12 +1,5 @@ import { Logo } from "@follow/components/icons/logo.jsx" import { Button } from "@follow/components/ui/button/index.jsx" -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@follow/components/ui/card/index.jsx" import { Form, FormControl, @@ -19,15 +12,18 @@ 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 { useTranslation } from "react-i18next" +import { Trans, useTranslation } from "react-i18next" +import { Link } from "react-router" import { toast } from "sonner" import { z } from "zod" export function Component() { return (
- - +
+ + +
) } @@ -36,10 +32,11 @@ const formSchema = z .object({ email: z.string().email(), password: z.string().min(8).max(128), - passwordConfirmation: z.string(), + confirmPassword: z.string(), }) - .refine((data) => data.password === data.passwordConfirmation, { - message: "Passwords do not match", + .refine((data) => data.password === data.confirmPassword, { + message: "Passwords don't match", + path: ["confirmPassword"], }) function onSubmit(values: z.infer) { @@ -67,62 +64,76 @@ function RegisterForm() { defaultValues: { email: "", password: "", - passwordConfirmation: "", + confirmPassword: "", }, }) return ( - - - {t("register.label")} - {t("register.description")} - - - - - ( - - {t("register.email")} - - - - - - )} - /> - ( - - {t("register.password")} - - - - - - )} - /> - ( - - {t("register.confirm_password")} - - - - - - )} - /> - - - - - +
+
+

+ {t("register.label", { app_name: APP_NAME })} +

+

+ + {t("register.login")} + + ), + }} + /> +

+
+
+ + ( + + {t("register.email")} + + + + + + )} + /> + ( + + {t("register.password")} + + + + + + )} + /> + ( + + {t("register.confirm_password")} + + + + + + )} + /> + + + +
) } diff --git a/locales/external/en.json b/locales/external/en.json index af6235de9f..40f771d002 100644 --- a/locales/external/en.json +++ b/locales/external/en.json @@ -55,9 +55,10 @@ "redirect.openApp": "Open {{app_name}}", "redirect.successMessage": "You have successfully connected to {{app_name}} Account.", "register.confirm_password": "Confirm Password", - "register.description": "Enter your email and password to create an account", "register.email": "Email", - "register.label": "Register", + "register.label": "Create a {{app_name}} account", + "register.login": "Log in", + "register.note": "Already have an account? ", "register.password": "Password", - "register.submit": "Submit" + "register.submit": "Create account" } From 62de1dfa049a5f3e5c3a47223829157310ba8ca2 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:52:51 +0800 Subject: [PATCH 22/36] chore: update --- apps/server/client/pages/(login)/login.tsx | 39 +++++++++---------- apps/server/client/pages/(login)/register.tsx | 2 +- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/apps/server/client/pages/(login)/login.tsx b/apps/server/client/pages/(login)/login.tsx index f20362b2ba..3483f76575 100644 --- a/apps/server/client/pages/(login)/login.tsx +++ b/apps/server/client/pages/(login)/login.tsx @@ -125,32 +125,29 @@ function Login() {
) : ( <> - {!isCredentialProvider && - Object.entries(authProviders || []) - .filter(([key]) => key !== "credential") - .map(([key, provider]) => ( - - ))} {!!authProviders?.credential && (
- {!isCredentialProvider && ( -

{t("login.or")}

- )} +

{t("login.or")}

)} + {Object.entries(authProviders || []) + .filter(([key]) => key !== "credential") + .map(([key, provider]) => ( + + ))} )} diff --git a/apps/server/client/pages/(login)/register.tsx b/apps/server/client/pages/(login)/register.tsx index 3fb2f7606c..5979017eb2 100644 --- a/apps/server/client/pages/(login)/register.tsx +++ b/apps/server/client/pages/(login)/register.tsx @@ -129,7 +129,7 @@ function RegisterForm() { )} /> - From 18bdec337be69482adf42a3b7396c69f019ca1c8 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:04:59 +0800 Subject: [PATCH 23/36] chore: update --- apps/server/client/pages/(login)/login.tsx | 34 +++++++++++++---- apps/server/client/pages/(login)/register.tsx | 38 ++++++++----------- locales/external/en.json | 4 +- 3 files changed, 45 insertions(+), 31 deletions(-) diff --git a/apps/server/client/pages/(login)/login.tsx b/apps/server/client/pages/(login)/login.tsx index 3483f76575..2db4864ec9 100644 --- a/apps/server/client/pages/(login)/login.tsx +++ b/apps/server/client/pages/(login)/login.tsx @@ -9,6 +9,7 @@ import { FormControl, FormField, FormItem, + FormLabel, FormMessage, } from "@follow/components/ui/form/index.jsx" import { Input } from "@follow/components/ui/input/index.js" @@ -19,8 +20,8 @@ import { cn } from "@follow/utils/utils" import { zodResolver } from "@hookform/resolvers/zod" import { useCallback, useEffect, useRef, useState } from "react" import { useForm } from "react-hook-form" -import { useTranslation } from "react-i18next" -import { useLocation } from "react-router" +import { Trans, useTranslation } from "react-i18next" +import { Link, useLocation } from "react-router" import { toast } from "sonner" import { z } from "zod" @@ -77,10 +78,25 @@ function Login() {
{!isAuthenticated && ( -

- {t("login.logInTo")} - {` ${APP_NAME}`} -

+
+

+ {t("login.logInTo")} + {` ${APP_NAME}`} +

+

+ + {t("login.register")} + + ), + }} + /> +

+
)} {redirecting ? (
{t("login.redirecting")}
@@ -188,8 +204,9 @@ function LoginWithPassword() { name="email" render={({ field }) => ( + {t("login.email")} - + @@ -200,8 +217,9 @@ function LoginWithPassword() { name="password" render={({ field }) => ( + {t("login.password")} - + diff --git a/apps/server/client/pages/(login)/register.tsx b/apps/server/client/pages/(login)/register.tsx index 5979017eb2..dd5c07c1e2 100644 --- a/apps/server/client/pages/(login)/register.tsx +++ b/apps/server/client/pages/(login)/register.tsx @@ -20,10 +20,8 @@ import { z } from "zod" export function Component() { return (
-
- - -
+ +
) } @@ -70,24 +68,20 @@ function RegisterForm() { return (
-
-

- {t("register.label", { app_name: APP_NAME })} -

-

- - {t("register.login")} - - ), - }} - /> -

-
+

{t("register.label", { app_name: APP_NAME })}

+

+ + {t("register.login")} + + ), + }} + /> +

", "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.", @@ -58,7 +60,7 @@ "register.email": "Email", "register.label": "Create a {{app_name}} account", "register.login": "Log in", - "register.note": "Already have an account? ", + "register.note": "Already have an account? ", "register.password": "Password", "register.submit": "Create account" } From f3720053deaed3acb8ca8678e6dd58765d4aa9a7 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:09:32 +0800 Subject: [PATCH 24/36] fix: email login handler --- packages/shared/src/auth.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/auth.ts b/packages/shared/src/auth.ts index fdfade2d85..444271b941 100644 --- a/packages/shared/src/auth.ts +++ b/packages/shared/src/auth.ts @@ -61,7 +61,10 @@ export const loginHandler = async ( window.open(`${WEB_URL}/login?provider=${provider}`) } else { if (provider === "credential") { - if (!email || !password) return + if (!email || !password) { + window.location.href = "/login" + return + } return signIn.email({ email, password }) } From b3d6932bc834a8f7bbb86d6ffea322e389af8c01 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:14:30 +0800 Subject: [PATCH 25/36] fix: navigate to login after register --- apps/server/client/pages/(login)/register.tsx | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/apps/server/client/pages/(login)/register.tsx b/apps/server/client/pages/(login)/register.tsx index dd5c07c1e2..b4e6021d69 100644 --- a/apps/server/client/pages/(login)/register.tsx +++ b/apps/server/client/pages/(login)/register.tsx @@ -13,7 +13,7 @@ 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 } from "react-router" +import { Link, useNavigate } from "react-router" import { toast } from "sonner" import { z } from "zod" @@ -37,23 +37,6 @@ const formSchema = z path: ["confirmPassword"], }) -function onSubmit(values: z.infer) { - return signUp.email({ - email: values.email, - password: values.password, - name: values.email.split("@")[0], - callbackURL: "/", - fetchOptions: { - onSuccess() { - window.location.href = "/" - }, - onError(context) { - toast.error(context.error.message) - }, - }, - }) -} - function RegisterForm() { const { t } = useTranslation("external") @@ -66,6 +49,25 @@ function RegisterForm() { }, }) + 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 (

{t("register.label", { app_name: APP_NAME })}

From 1f02560999e7fd125b7391e65b38e7515e16fbbe Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:19:55 +0800 Subject: [PATCH 26/36] chore: remove forget password button for now --- .../src/modules/profile/update-password-form.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/renderer/src/modules/profile/update-password-form.tsx b/apps/renderer/src/modules/profile/update-password-form.tsx index 22dc6af6af..6bb31bdd10 100644 --- a/apps/renderer/src/modules/profile/update-password-form.tsx +++ b/apps/renderer/src/modules/profile/update-password-form.tsx @@ -20,6 +20,7 @@ import { z } from "zod" import { useWhoami } from "~/atoms/user" import { useHasPassword } from "~/queries/auth" +// eslint-disable-next-line unused-imports/no-unused-vars const ForgetPasswordButton = () => { const { t } = useTranslation("settings") const user = useWhoami() @@ -170,12 +171,12 @@ const UpdateExistingPasswordForm = () => { export const UpdatePasswordForm = () => { const { data: hasPassword, isLoading } = useHasPassword() - if (isLoading) { + if (isLoading || !hasPassword) { return null } - if (!hasPassword) { - return - } + // if (!hasPassword) { + // return + // } return } From 03fc0ffa7fcb2ce2a7b9da5b24dc16142c24c577 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:21:03 +0800 Subject: [PATCH 27/36] chore: update --- changelog/next.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 5010459655..d7f8b9d6fb 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -2,10 +2,7 @@ ## New Features -- New `Move to Category` operation in feed subscription context menu -- New `Expand long social media` setting to automatically expand social media entries containing long text. -- New `Back Top` button and read progress indicator in entry content -- Login with email and password +- Register or Login with email and password ## Improvements From 0b6580530926a029b9b043242142faaf86d0d42f Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:39:07 +0800 Subject: [PATCH 28/36] feat: forget password page --- .../modules/profile/update-password-form.tsx | 46 +-------- .../client/pages/(login)/forget-password.tsx | 96 +++++++++++++++++++ .../client/pages/(login)/reset-password.tsx | 18 ++-- locales/external/en.json | 5 +- locales/settings/en.json | 1 - 5 files changed, 109 insertions(+), 57 deletions(-) create mode 100644 apps/server/client/pages/(login)/forget-password.tsx diff --git a/apps/renderer/src/modules/profile/update-password-form.tsx b/apps/renderer/src/modules/profile/update-password-form.tsx index 6bb31bdd10..c3dc2e4b52 100644 --- a/apps/renderer/src/modules/profile/update-password-form.tsx +++ b/apps/renderer/src/modules/profile/update-password-form.tsx @@ -8,8 +8,7 @@ import { FormMessage, } from "@follow/components/ui/form/index.jsx" import { Input } from "@follow/components/ui/input/index.js" -import { changePassword, forgetPassword } from "@follow/shared/auth" -import { env } from "@follow/shared/env" +import { changePassword } from "@follow/shared/auth" import { zodResolver } from "@hookform/resolvers/zod" import { useMutation } from "@tanstack/react-query" import { useForm } from "react-hook-form" @@ -17,48 +16,8 @@ import { useTranslation } from "react-i18next" import { toast } from "sonner" import { z } from "zod" -import { useWhoami } from "~/atoms/user" import { useHasPassword } from "~/queries/auth" -// eslint-disable-next-line unused-imports/no-unused-vars -const ForgetPasswordButton = () => { - const { t } = useTranslation("settings") - const user = useWhoami() - const forgetPasswordMutation = useMutation({ - mutationFn: async () => { - if (!user) { - throw new Error("No user found") - } - const res = await forgetPassword({ - email: user.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(t("profile.reset_password_mail_sent"), { - duration: 3000, - }) - }, - }) - - return ( - - ) -} - const passwordSchema = z.string().min(8).max(128) const updatePasswordFormSchema = z @@ -175,8 +134,5 @@ export const UpdatePasswordForm = () => { return null } - // if (!hasPassword) { - // return - // } 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..508e4b6f1b --- /dev/null +++ b/apps/server/client/pages/(login)/forget-password.tsx @@ -0,0 +1,96 @@ +import { Button } 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 { 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 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) + } + + return ( +
+ + + {t("login.forget_password.label")} + {t("login.forget_password.description")} + + + + + ( + + {t("login.email")} + + + + + + )} + /> +
+ +
+ + +
+
+
+ ) +} diff --git a/apps/server/client/pages/(login)/reset-password.tsx b/apps/server/client/pages/(login)/reset-password.tsx index 4e61ee1996..dce2325e5a 100644 --- a/apps/server/client/pages/(login)/reset-password.tsx +++ b/apps/server/client/pages/(login)/reset-password.tsx @@ -11,6 +11,7 @@ import { FormControl, FormField, FormItem, + FormLabel, FormMessage, } from "@follow/components/ui/form/index.jsx" import { Input } from "@follow/components/ui/input/index.js" @@ -19,6 +20,7 @@ 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" @@ -43,6 +45,7 @@ export function Component() { }, }) + const navigate = useNavigate() const updateMutation = useMutation({ mutationFn: async (values: z.infer) => { const res = await resetPassword({ newPassword: values.newPassword }) @@ -56,6 +59,7 @@ export function Component() { }, onSuccess: () => { toast.success(t("login.reset_password.success")) + navigate("/login") }, }) @@ -78,12 +82,9 @@ export function Component() { name="newPassword" render={({ field }) => ( + {t("login.new_password.label")} - + @@ -94,12 +95,9 @@ export function Component() { name="confirmPassword" render={({ field }) => ( + {t("login.confirm_password.label")} - + diff --git a/locales/external/en.json b/locales/external/en.json index aa11988020..0271d1a378 100644 --- a/locales/external/en.json +++ b/locales/external/en.json @@ -37,6 +37,9 @@ "login.confirm_password.label": "Confirm Password", "login.continueWith": "Continue with {{provider}}", "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.success": "Email has been sent successfully", "login.logIn": "Log in", "login.logInTo": "Log in to ", "login.new_password.label": "New Password", @@ -48,7 +51,7 @@ "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.reset_password.success": "Password has been successfully reset", "login.signOut": "Sign out", "login.submit": "Submit", "login.welcomeTo": "Welcome to ", diff --git a/locales/settings/en.json b/locales/settings/en.json index b53e1d1bff..1c770ab962 100644 --- a/locales/settings/en.json +++ b/locales/settings/en.json @@ -263,7 +263,6 @@ "profile.confirm_password.label": "Confirm Password", "profile.current_password.label": "Current Password", "profile.email.label": "Email", - "profile.forget_password": "Forget Password", "profile.handle.description": "Your unique identifier.", "profile.handle.label": "Handle", "profile.name.description": "Your public display name.", From 1096d498084da935bb48c0215b749abf47411637 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Mon, 16 Dec 2024 16:10:41 +0800 Subject: [PATCH 29/36] chore: update hono --- packages/shared/src/hono.ts | 551 +++++++++++++++++++++++------------- 1 file changed, 360 insertions(+), 191 deletions(-) diff --git a/packages/shared/src/hono.ts b/packages/shared/src/hono.ts index fe95dd0204..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; }; - session: { + email: { + type: string; + description: string; + }; + 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]>>; + 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; @@ -9046,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>; @@ -9094,7 +9263,7 @@ declare const _routes: hono_hono_base.HonoBase Date: Mon, 16 Dec 2024 16:18:01 +0800 Subject: [PATCH 30/36] fix: forget-password link --- apps/server/client/pages/(login)/login.tsx | 9 +++++++-- apps/server/client/pages/(login)/register.tsx | 2 +- locales/external/en.json | 1 + 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/server/client/pages/(login)/login.tsx b/apps/server/client/pages/(login)/login.tsx index 2db4864ec9..0b247074c0 100644 --- a/apps/server/client/pages/(login)/login.tsx +++ b/apps/server/client/pages/(login)/login.tsx @@ -89,7 +89,7 @@ function Login() { i18nKey="login.note" components={{ RegisterLink: ( - + {t("login.register")} ), @@ -217,7 +217,12 @@ function LoginWithPassword() { name="password" render={({ field }) => ( - {t("login.password")} + + {t("login.password")} + + {t("login.forget_password.note")} + + diff --git a/apps/server/client/pages/(login)/register.tsx b/apps/server/client/pages/(login)/register.tsx index b4e6021d69..d81d76944c 100644 --- a/apps/server/client/pages/(login)/register.tsx +++ b/apps/server/client/pages/(login)/register.tsx @@ -77,7 +77,7 @@ function RegisterForm() { i18nKey="register.note" components={{ LoginLink: ( - + {t("register.login")} ), diff --git a/locales/external/en.json b/locales/external/en.json index 0271d1a378..e1b02858c1 100644 --- a/locales/external/en.json +++ b/locales/external/en.json @@ -39,6 +39,7 @@ "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.logIn": "Log in", "login.logInTo": "Log in to ", From 678287e8398ea2374a1772f9bc522af64c5b10a0 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Mon, 16 Dec 2024 17:11:01 +0800 Subject: [PATCH 31/36] feat: login email text --- apps/server/client/pages/(login)/login.tsx | 71 +++++++++++----------- locales/external/en.json | 4 +- 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/apps/server/client/pages/(login)/login.tsx b/apps/server/client/pages/(login)/login.tsx index 0b247074c0..31c7a72c08 100644 --- a/apps/server/client/pages/(login)/login.tsx +++ b/apps/server/client/pages/(login)/login.tsx @@ -4,6 +4,7 @@ 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 { Divider } from "@follow/components/ui/divider/index.js" import { Form, FormControl, @@ -21,7 +22,7 @@ import { zodResolver } from "@hookform/resolvers/zod" import { useCallback, useEffect, useRef, useState } from "react" import { useForm } from "react-hook-form" import { Trans, useTranslation } from "react-i18next" -import { Link, useLocation } from "react-router" +import { Link, useLocation, useNavigate } from "react-router" import { toast } from "sonner" import { z } from "zod" @@ -78,25 +79,10 @@ function Login() {
{!isAuthenticated && ( -
-

- {t("login.logInTo")} - {` ${APP_NAME}`} -

-

- - {t("login.register")} - - ), - }} - /> -

-
+

+ {t("login.logInTo")} + {` ${APP_NAME}`} +

)} {redirecting ? (
{t("login.redirecting")}
@@ -141,12 +127,6 @@ function Login() {
) : ( <> - {!!authProviders?.credential && ( -
- -

{t("login.or")}

-
- )} {Object.entries(authProviders || []) .filter(([key]) => key !== "credential") .map(([key, provider]) => ( @@ -164,6 +144,18 @@ function Login() { {t("login.continueWith", { provider: provider.name })} ))} + {!!authProviders?.credential && ( +
+
+ +

+ {t("login.or")} +

+ +
+ +
+ )} )}
@@ -195,10 +187,11 @@ function LoginWithPassword() { password: "", }, }) + const navigate = useNavigate() return (
- + ( - - {t("login.password")} - - {t("login.forget_password.note")} - - + {t("login.password")} @@ -230,8 +218,21 @@ function LoginWithPassword() { )} /> - + diff --git a/locales/external/en.json b/locales/external/en.json index e1b02858c1..6629eb8cd2 100644 --- a/locales/external/en.json +++ b/locales/external/en.json @@ -41,10 +41,9 @@ "login.forget_password.label": "Forget Password", "login.forget_password.note": "Forgot your password?", "login.forget_password.success": "Email has been sent successfully", - "login.logIn": "Log in", "login.logInTo": "Log in to ", + "login.logInWithEmail": "Log in with email", "login.new_password.label": "New Password", - "login.note": "Don't have an account? ", "login.openApp": "Open App", "login.or": "Or", "login.password": "Password", @@ -54,6 +53,7 @@ "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", From dceec2cd610d1df9772d00dcfa0b925e60a06b46 Mon Sep 17 00:00:00 2001 From: Innei Date: Mon, 16 Dec 2024 18:12:41 +0800 Subject: [PATCH 32/36] refactor: enhance login and forget password functionality - Updated the forget password page to include a back navigation button using MotionButtonBase. - Refactored the login component to utilize the new Login module, simplifying the structure. - Adjusted translations for consistency in the login text across English and German locales. - Improved the useAuthProviders hook to return a more structured AuthProvider interface. Signed-off-by: Innei --- apps/server/client/modules/login/index.tsx | 257 ++++++++++++++++++ .../client/pages/(login)/forget-password.tsx | 21 +- apps/server/client/pages/(login)/login.tsx | 237 +--------------- apps/server/client/query/users.ts | 19 +- locales/external/de.json | 2 +- locales/external/en.json | 6 +- 6 files changed, 287 insertions(+), 255 deletions(-) create mode 100644 apps/server/client/modules/login/index.tsx diff --git a/apps/server/client/modules/login/index.tsx b/apps/server/client/modules/login/index.tsx new file mode 100644 index 0000000000..4384dc9e08 --- /dev/null +++ b/apps/server/client/modules/login/index.tsx @@ -0,0 +1,257 @@ +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 } 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" + +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: { + return ( + <> + {Object.entries(authProviders || []) + .filter(([key]) => key !== "credential") + .map(([key, provider]) => ( + + ))} + {!!authProviders?.credential && ( +
+
+ +

{t("login.or")}

+ +
+ +
+ )} + + ) + } + } + }, [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().min(8).max(128), +}) + +async function onSubmit(values: z.infer) { + const res = await loginHandler("credential", 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 navigate = useNavigate() + + return ( +
+ + ( + + {t("login.email")} + + + + + + )} + /> + ( + + {t("login.password")} + + + + + + )} + /> + + {t("login.forget_password.note")} + + + + + + ) +} diff --git a/apps/server/client/pages/(login)/forget-password.tsx b/apps/server/client/pages/(login)/forget-password.tsx index 508e4b6f1b..9bdb3a9950 100644 --- a/apps/server/client/pages/(login)/forget-password.tsx +++ b/apps/server/client/pages/(login)/forget-password.tsx @@ -1,4 +1,4 @@ -import { Button } from "@follow/components/ui/button/index.jsx" +import { Button, MotionButtonBase } from "@follow/components/ui/button/index.jsx" import { Card, CardContent, @@ -21,6 +21,7 @@ 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" @@ -59,14 +60,28 @@ export function Component() { updateMutation.mutate(values) } + const navigate = useNavigate() + return (
- {t("login.forget_password.label")} - {t("login.forget_password.description")} + + { + history.length > 1 ? history.back() : navigate("/login") + }} + className="inline-flex cursor-pointer items-center" + > + + + {t("login.forget_password.label")} + + + {t("login.forget_password.description")} +
} - -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 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]) - - 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 || []) - .filter(([key]) => key !== "credential") - .map(([key, provider]) => ( - - ))} - {!!authProviders?.credential && ( -
-
- -

- {t("login.or")} -

- -
- -
- )} - - )} -
- )} -
- ) -} - -const formSchema = z.object({ - email: z.string().email(), - password: z.string().min(8).max(128), -}) - -async function onSubmit(values: z.infer) { - const res = await loginHandler("credential", 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 navigate = useNavigate() - - return ( - - - ( - - {t("login.email")} - - - - - - )} - /> - ( - - {t("login.password")} - - - - - - )} - /> - - {t("login.forget_password.note")} - - - - - - ) -} 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/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 6629eb8cd2..a0ac35eb35 100644 --- a/locales/external/en.json +++ b/locales/external/en.json @@ -41,8 +41,8 @@ "login.forget_password.label": "Forget Password", "login.forget_password.note": "Forgot your password?", "login.forget_password.success": "Email has been sent successfully", - "login.logInTo": "Log in to ", - "login.logInWithEmail": "Log in with email", + "login.logInTo": "Login to ", + "login.logInWithEmail": "Login with email", "login.new_password.label": "New Password", "login.openApp": "Open App", "login.or": "Or", @@ -63,7 +63,7 @@ "register.confirm_password": "Confirm Password", "register.email": "Email", "register.label": "Create a {{app_name}} account", - "register.login": "Log in", + "register.login": "Login", "register.note": "Already have an account? ", "register.password": "Password", "register.submit": "Create account" From 704e1102f6e5163ca90ec9c16b4234dd1784c54c Mon Sep 17 00:00:00 2001 From: Innei Date: Mon, 16 Dec 2024 18:20:14 +0800 Subject: [PATCH 33/36] feat: add form validation and UI enhancements for login-related pages - Introduced form validation state management in forget-password, register, and reset-password components. - Updated button states to be disabled when forms are invalid, improving user experience. - Enhanced UI elements with consistent styling and layout adjustments, including the addition of MotionButtonBase for navigation. - Improved accessibility and responsiveness of card components. Signed-off-by: Innei --- .../client/pages/(login)/forget-password.tsx | 7 +++--- apps/server/client/pages/(login)/register.tsx | 16 +++++++++----- .../client/pages/(login)/reset-password.tsx | 22 ++++++++++++++----- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/apps/server/client/pages/(login)/forget-password.tsx b/apps/server/client/pages/(login)/forget-password.tsx index 9bdb3a9950..7b1f800c83 100644 --- a/apps/server/client/pages/(login)/forget-password.tsx +++ b/apps/server/client/pages/(login)/forget-password.tsx @@ -38,6 +38,7 @@ export function Component() { }, }) + const { isValid } = form.formState const updateMutation = useMutation({ mutationFn: async (values: z.infer) => { const res = await forgetPassword({ @@ -64,14 +65,14 @@ export function Component() { return (
- + { history.length > 1 ? history.back() : navigate("/login") }} - className="inline-flex cursor-pointer items-center" + className="-ml-1 inline-flex cursor-pointer items-center" > @@ -98,7 +99,7 @@ export function Component() { )} />
-
diff --git a/apps/server/client/pages/(login)/register.tsx b/apps/server/client/pages/(login)/register.tsx index d81d76944c..9066b8a657 100644 --- a/apps/server/client/pages/(login)/register.tsx +++ b/apps/server/client/pages/(login)/register.tsx @@ -49,6 +49,8 @@ function RegisterForm() { }, }) + const { isValid } = form.formState + const navigate = useNavigate() function onSubmit(values: z.infer) { @@ -69,9 +71,11 @@ function RegisterForm() { } return ( -
-

{t("register.label", { app_name: APP_NAME })}

-

+

+

+ {t("register.label", { app_name: APP_NAME })} +

+
-

+
- + )} /> - diff --git a/apps/server/client/pages/(login)/reset-password.tsx b/apps/server/client/pages/(login)/reset-password.tsx index dce2325e5a..2dac23add9 100644 --- a/apps/server/client/pages/(login)/reset-password.tsx +++ b/apps/server/client/pages/(login)/reset-password.tsx @@ -1,4 +1,4 @@ -import { Button } from "@follow/components/ui/button/index.jsx" +import { Button, MotionButtonBase } from "@follow/components/ui/button/index.jsx" import { Card, CardContent, @@ -45,6 +45,8 @@ export function Component() { }, }) + const { isValid } = form.formState + const navigate = useNavigate() const updateMutation = useMutation({ mutationFn: async (values: z.infer) => { @@ -69,12 +71,22 @@ export function Component() { return (
- + - {t("login.reset_password.label")} - {t("login.reset_password.description")} + + { + history.length > 1 ? history.back() : navigate("/login") + }} + className="-ml-1 inline-flex cursor-pointer items-center" + > + + + {t("login.forget_password.label")} + + {t("login.reset_password.description")}
-
From 403a98f1ef1bffcbd38929b0ee4767de494e9107 Mon Sep 17 00:00:00 2001 From: Innei Date: Mon, 16 Dec 2024 18:31:52 +0800 Subject: [PATCH 34/36] feat: enhance login component with dynamic provider buttons - Added MotionButtonBase for improved button animations and interactions. - Refactored the rendering logic to conditionally display login options based on the presence of credential providers. - Introduced a new icon mapping for providers to enhance visual representation. - Improved layout and styling for better user experience during login. Signed-off-by: Innei --- apps/server/client/modules/login/index.tsx | 88 ++++++++++++++++------ 1 file changed, 63 insertions(+), 25 deletions(-) diff --git a/apps/server/client/modules/login/index.tsx b/apps/server/client/modules/login/index.tsx index 4384dc9e08..a285c86762 100644 --- a/apps/server/client/modules/login/index.tsx +++ b/apps/server/client/modules/login/index.tsx @@ -4,7 +4,7 @@ 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 } from "@follow/components/ui/button/index.js" +import { Button, MotionButtonBase } from "@follow/components/ui/button/index.js" import { Divider } from "@follow/components/ui/divider/index.js" import { Form, @@ -28,12 +28,17 @@ import { Link, useLocation, useNavigate } from "react-router" import { toast } from "sonner" import { z } from "zod" +const overrideProviderIconMap: Record = { + apple: , +} + 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") @@ -116,37 +121,70 @@ export function Login() { ) } default: { - return ( - <> - {Object.entries(authProviders || []) - .filter(([key]) => key !== "credential") - .map(([key, provider]) => ( - - ))} - {!!authProviders?.credential && ( + if (!authProviders?.credential) { + return ( +
+ {Object.entries(authProviders || []) + .filter(([key]) => key !== "credential") + .map(([key, provider]) => ( + + ))} +
+ ) + } else { + return ( + <> +

{t("login.or")}

-
- )} - - ) +
+ {Object.entries(authProviders || []) + .filter(([key]) => key !== "credential") + .map(([key, provider]) => ( + { + loginHandler(key) + }} + > + {overrideProviderIconMap[provider.id] ? ( +
+ {overrideProviderIconMap[provider.id]} +
+ ) : ( +
+ )} + + ))} +
+ + ) + } } } }, [authProviders, handleOpenApp, isAuthenticated, refetch, t]) From 06b3c0c806a5a9ef0aa06212102318c2e5a40419 Mon Sep 17 00:00:00 2001 From: Innei Date: Mon, 16 Dec 2024 18:44:50 +0800 Subject: [PATCH 35/36] feat: add GitHub provider icon and adjust button margin in login component - Introduced a GitHub icon to the provider icon map for enhanced visual representation. - Adjusted the margin of the submit button to improve layout consistency. Signed-off-by: Innei --- apps/server/client/modules/login/index.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/server/client/modules/login/index.tsx b/apps/server/client/modules/login/index.tsx index a285c86762..b21bf81ed4 100644 --- a/apps/server/client/modules/login/index.tsx +++ b/apps/server/client/modules/login/index.tsx @@ -30,6 +30,7 @@ import { z } from "zod" const overrideProviderIconMap: Record = { apple: , + github: , } export function Login() { @@ -221,7 +222,7 @@ export function Login() { const formSchema = z.object({ email: z.string().email(), - password: z.string().min(8).max(128), + password: z.string().max(128), }) async function onSubmit(values: z.infer) { @@ -242,6 +243,7 @@ function LoginWithPassword() { password: "", }, }) + const { isValid } = form.formState const navigate = useNavigate() return ( @@ -276,7 +278,12 @@ function LoginWithPassword() { {t("login.forget_password.note")} -