Skip to content

Commit

Permalink
feat: register or login with email and password (#2075)
Browse files Browse the repository at this point in the history
* feat: login with password

* chore: auto-fix linting and formatting issues

* feat: show email

* refactor: login form

* chore: update

* fix: show login error

* chore: update

* credential provider

* feat: confirm password

* chore: update

* revokeOtherSessions when update password

* changelog

* typecheck

* chore: update

* chore: update hono

* feat: forget password

* chore: update

* feat: reset password page

* feat: register form

* chore: update

* chore: update

* chore: update

* chore: update

* fix: email login handler

* fix: navigate to login after register

* chore: remove forget password button for now

* chore: update

* feat: forget password page

* chore: update hono

* fix: forget-password link

* feat: login email text

* 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 <tukon479@gmail.com>

* 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 <tukon479@gmail.com>

* 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 <tukon479@gmail.com>

* 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 <tukon479@gmail.com>

* chore: update

---------

Signed-off-by: Innei <tukon479@gmail.com>
Co-authored-by: hyoban <hyoban@users.noreply.github.com>
Co-authored-by: DIYgod <i@diygod.me>
Co-authored-by: Innei <tukon479@gmail.com>
  • Loading branch information
4 people authored Dec 16, 2024
1 parent 45e37a0 commit 7a6edb4
Show file tree
Hide file tree
Showing 18 changed files with 1,296 additions and 341 deletions.
5 changes: 5 additions & 0 deletions apps/renderer/src/modules/profile/profile-setting-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
FormMessage,
} from "@follow/components/ui/form/index.jsx"
import { Input } from "@follow/components/ui/input/index.js"
import { Label } from "@follow/components/ui/label/index.js"
import { updateUser } from "@follow/shared/auth"
import { cn } from "@follow/utils/utils"
import { zodResolver } from "@hookform/resolvers/zod"
Expand Down Expand Up @@ -74,6 +75,10 @@ export const ProfileSettingForm = ({
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className={cn("mt-4 space-y-4", className)}>
<div className="space-y-2">
<Label>{t("profile.email.label")}</Label>
<p className="text-sm text-muted-foreground">{user?.email}</p>
</div>
<FormField
control={form.control}
name="handle"
Expand Down
138 changes: 138 additions & 0 deletions apps/renderer/src/modules/profile/update-password-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
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 { useHasPassword } from "~/queries/auth"

const passwordSchema = z.string().min(8).max(128)

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")

const form = useForm<z.infer<typeof updatePasswordFormSchema>>({
resolver: zodResolver(updatePasswordFormSchema),
defaultValues: {
currentPassword: "",
newPassword: "",
confirmPassword: "",
},
})

const updateMutation = useMutation({
mutationFn: async (values: z.infer<typeof updatePasswordFormSchema>) => {
const res = await changePassword({
currentPassword: values.currentPassword,
newPassword: values.newPassword,
revokeOtherSessions: true,
})
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<typeof updatePasswordFormSchema>) {
updateMutation.mutate(values)
}

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="mt-4 space-y-4">
<FormField
control={form.control}
name="currentPassword"
render={({ field }) => (
<FormItem>
<FormLabel>{t("profile.change_password.label")}</FormLabel>
<FormControl>
<Input
type="password"
placeholder={t("profile.current_password.label")}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="newPassword"
render={({ field }) => (
<FormItem>
<FormControl>
<Input type="password" placeholder={t("profile.new_password.label")} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
type="password"
placeholder={t("profile.confirm_password.label")}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="text-right">
<Button type="submit" isLoading={updateMutation.isPending}>
{t("profile.submit")}
</Button>
</div>
</form>
</Form>
)
}

export const UpdatePasswordForm = () => {
const { data: hasPassword, isLoading } = useHasPassword()

if (isLoading || !hasPassword) {
return null
}

return <UpdateExistingPasswordForm />
}
2 changes: 2 additions & 0 deletions apps/renderer/src/pages/settings/(settings)/profile.tsx
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -15,6 +16,7 @@ export function Component() {
<>
<SettingsTitle />
<ProfileSettingForm />
<UpdatePasswordForm />
</>
)
}
19 changes: 18 additions & 1 deletion apps/renderer/src/queries/auth.ts
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -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 }) => {
Expand Down
Loading

0 comments on commit 7a6edb4

Please sign in to comment.