fix: feature-base-refactoring (#297)

* docs: add AGENTS.md with feature-based architecture conventions

Defines the target src/ structure, feature list, naming conventions
(kebab-case, file type patterns), import direction rules, and
feature boundary guidelines for the planned big-bang refactor.

* docs: add features/channel/ to architecture conventions

Generic channel UI (card, modal, form) goes to features/channel/.
Provider-specific sub-folders: channel/notifications/ and channel/storages/.

* refactor: move event emitter to lib/, project service to db/services/, dedup use-mobile hook

* fix: remove unused auth import in db/services/project.ts

* refactor: migrate wrappers/common to components/common (flat)

* fix: remove dead commented-out code from button-with-loading.tsx

* refactor: merge features/browser into features/theme, flatten upload/updates, migrate statistics

* refactor: migrate wrappers/auth to features/auth

Move all auth components and forms from src/components/wrappers/auth/
into src/features/auth/ with flat kebab-case naming. Handle extra
reset-password-form.tsx and differing reset-password-schema.ts by
copying them as reset-password-page-form.tsx and reset-password-page.schema.ts.

* refactor: remove dead auth files and fix zPassword schema

Delete reset-password-section.tsx and reset-password-page-form.tsx (zero consumers).
Fix reset-password-page.schema.ts to import zPassword from @/lib/zod.

* refactor: migrate wrappers/dashboard/common to features/layout, rename Header.tsx to header.tsx

* fix: decouple layout from theme/updates cross-feature imports

Use slot props (actions, updateNotification) so app/ composes
ModeToggle and UpdateNotification into Header and AppSidebar.
Delete dead side-bar-logo.tsx (zero consumers).

* refactor: merge features/keys into features/agents, flatten sub-dirs, migrate agent wrappers

- Merge src/features/keys/keys.action.ts into src/features/agents/
- Flatten src/features/agents/components/ and hooks/ into feature root
- Migrate all src/components/wrappers/dashboard/agent/ files into src/features/agents/
- Update all import paths across app/ and src/ accordingly
- Fix broken code-snippet import in agent-key-modal.tsx (was pointing to non-existent wrappers path)

* fix: move github-releases to lib/, remove dead code and unused imports

Move GitHub release fetch logic from features/updates/github.ts to
lib/github-releases.ts so both features/updates and features/agents
can import it without cross-feature imports.
Remove commented-out impersonation block from agent-database-card.tsx.
Remove unused imports (CopyButton, Terminal, Key, Info).

* refactor: flatten features/projects/components, migrate project wrappers

Move project.dialog/form out of sub-dir, migrate project-card, project-database-card, and delete-project button/action from wrappers into features/projects/. Update all import paths across src/ and app/. Leave wrappers/dashboard/projects/database/ untouched for Task 10.

* fix: extract DatabaseCard to components/common, remove cross-feature import

DatabaseCard was imported by both features/projects and features/agents,
violating the no-cross-feature-import rule. Extract to components/common/
database-card.tsx so both features import from the shared layer.
Also tighten extendedProps type and remove commented-out maxCount prop.

* refactor: migrate all database/backup/restore/health UI to features/database

Consolidates ~40 files from 5 source locations (features/dashboard/backup,
features/dashboard/restore, wrappers/dashboard/backup, wrappers/dashboard/database,
wrappers/dashboard/health, wrappers/dashboard/projects/database) into the new
flat features/database directory with kebab-case filenames and updated @/ imports.

* fix: remove dead restore-form files and console.log from health-grid

Delete restore-form.tsx (186 lines all commented-out, zero consumers)
and restore-form.schema.ts (only a commented import).
Remove console.log(logs) from getOldestLog helper in health-grid.tsx.

Cross-feature imports (database→storages for dispatchStorage/storeBackupFiles)
are accepted as known architectural coupling — storage dispatch is called from
database server actions at runtime and cannot route through app/ props.

* refactor: migrate wrappers/admin/users to features/users, fix table-colums typo

* fix: fix relative imports, string interpolation, and console.error in users feature

Replace relative imports with @/ aliases in user-actions-cell.tsx and
admin-user-edit-modal.tsx. Fix string interpolation bug in dialog title
(was regular string, now template literal). Remove console.error from
admin-user-change-role-modal.tsx.

* refactor: migrate wrappers/profile to features/profile, fix profile-apperance typo

* fix: remove console.error calls and ts-ignore in profile feature

Remove console.error from profile-account.tsx, 2fa-form.tsx (x2).
Replace @ts-ignore on fileInput.onchange with typed cast in avatar-with-upload.tsx.

* refactor: rename features/organization to features/organizations, migrate all org wrappers

Moves 37 org-related files from features/organization/, features/dashboard/,
hooks/, wrappers/admin/organizations/, wrappers/admin/channels/organization/,
and wrappers/dashboard/organization/ into a flat features/organizations/ feature
directory. Updates all import paths codebase-wide. Fixes @ts-ignore and removes
unused @better-fetch/fetch import in admin-organization-form.

* fix: remove console.error, @ts-ignore, and unused destructures in features/organizations

* refactor: migrate wrappers/organization/migration to features/migration

Move all 5 migration files to src/features/migration/, update internal
imports to @/features/migration/ absolute paths, remove console.log,
and delete src/components/wrappers/dashboard/organization/ entirely.

* refactor: migrate wrappers/admin/settings to features/settings

Move all 14 settings files (email, notification, storage, s3) from
wrappers/dashboard/admin/settings/ into features/settings/, rewriting
imports, replacing @ts-ignore with @ts-expect-error, and renaming error
params to _error where applicable.

* refactor: rename notifications files to kebab convention, migrate notification logs

- Rename dispatch.ts/helpers.ts/types.ts to notifications.*.ts
- Migrate notification logs components from wrappers/dashboard/admin/notifications/logs/ to features/notifications/
- Fix all import paths across codebase and in providers/
- Remove unused useRouter import, replace @ts-ignore with @ts-expect-error
- Delete src/components/wrappers/dashboard/admin/notifications/

* refactor: rename storages dispatch/helpers/types to kebab convention

- Rename dispatch.ts/helpers.ts/types.ts to storages.*.ts
- Fix all import paths across codebase and in providers/
- Fix relative ../types and ../../types imports in all storage providers to absolute @/ paths
- Replace @ts-ignore with @ts-expect-error, remove console.debug/console.error

* refactor: create features/channel with generic UI and notifications/storages provider sub-folders

Consolidates 4 source locations into src/features/channel/:
- components/wrappers/dashboard/admin/channels/ → features/channel/ (11 generic UI files)
- features/notifications/providers/ → features/channel/notifications/ (9 backend + 18 form files)
- features/storages/providers/ → features/channel/storages/ (backend + forms)

The two conflicting google-drive/helpers.ts files (client helpers vs OAuth action)
are kept separate: helpers.ts and google-drive-refresh.action.ts.

Rewrites all imports codebase-wide to point to new locations.
Removes console.log/console.error calls, upgrades @ts-ignore to @ts-expect-error.

* fix: remove console.error from local storage provider

* fix: multi-select.tsx scroll issue

* fix: multi-select.tsx scroll issue
This commit is contained in:
Charles GTE
2026-05-24 17:09:12 +02:00
committed by GitHub
parent 4fadfcfaf2
commit 3fa0f9b76d
330 changed files with 627 additions and 1120 deletions
+46
View File
@@ -0,0 +1,46 @@
"use client";
import {env} from "@/env.mjs";
import {useTheme} from "next-themes";
import Image from "next/image";
import {useEffect, useState} from "react";
export const AuthLogoSection = () => {
const {resolvedTheme} = useTheme();
const [mounted, setMounted] = useState(false);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const imageTheme =
resolvedTheme === "dark"
? "/images/logo-dark.png"
: "/images/logo-light.png";
const handleLoad = () => setLoaded(true);
const style = {
transition: "opacity 0.3s ease-in-out",
opacity: loaded ? 1 : 0,
};
return (
<div className="sm:mx-auto sm:w-full sm:max-w-md relative flex items-center justify-center h-[160px]">
{mounted && (
<Image
src={imageTheme}
alt="Logo"
fill
priority
className="object-contain p-10"
onLoad={handleLoad}
style={style}
/>
)}
</div>
);
};
@@ -0,0 +1,10 @@
"use client";
import { z } from "zod";
import { zEmail } from "@/lib/zod";
export const ForgotPasswordSchema = z.object({
email: zEmail(),
});
export type ForgotPasswordType = z.infer<typeof ForgotPasswordSchema>;
@@ -0,0 +1,81 @@
"use client";
import { useMutation } from "@tanstack/react-query";
import { toast } from "sonner";
import { FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Form } from "@/components/ui/form";
import { requestPasswordReset } from "@/lib/auth/auth-client";
import { ButtonWithLoading } from "@/components/common/button-with-loading";
import Link from "next/link";
import { ForgotPasswordSchema, ForgotPasswordType } from "./forgot-password-form.schema";
import { ArrowLeft } from "lucide-react";
export type ForgotPasswordFormProps = {
defaultValues?: ForgotPasswordType;
};
export const ForgotPasswordForm = (props: ForgotPasswordFormProps) => {
const form = useZodForm({
schema: ForgotPasswordSchema,
});
const mutation = useMutation({
mutationFn: async (values: ForgotPasswordType) => {
await requestPasswordReset(
{
email: values.email,
},
{
onSuccess: () => {
toast.success("If an account with this email address exists, you will receive an email with instructions to reset your password.");
},
onError: (error) => {
toast.error(error.error.message);
},
}
);
},
});
return (
<Form
form={form}
className="flex flex-col gap-4 mb-1"
onSubmit={async (values) => {
await mutation.mutateAsync(values);
}}
>
<FormField
control={form.control}
name="email"
defaultValue=""
render={({ field }) => (
<FormItem>
<FormLabel>Email Address</FormLabel>
<FormControl>
<Input autoComplete="email" autoFocus
placeholder="example@portabase.io"
{...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex flex-col items-center gap-y-6 w-full">
<ButtonWithLoading className="mt-2 w-full h-11" isPending={mutation.isPending}>
Send reset link
</ButtonWithLoading>
<Link href="/login" className="group flex items-center text-sm hover:underline">
<ArrowLeft className="mr-1 size-4 text-muted-foreground transition-transform group-hover:-translate-x-1" />
Back to login
</Link>
</div>
</Form>
);
};
@@ -0,0 +1,75 @@
"use server";
import { ServerActionResult } from "@/types/action-type";
import { auth } from "@/lib/auth/auth";
import { zString } from "@/lib/zod";
import z from "zod";
import { db } from "@/db";
import {action} from "@/lib/safe-actions/actions";
//TODO: to be continued...
export const forgotPasswordAction = action
.schema(
z.object({
schema: z.object({
email: zString(),
}),
redirectTo: zString().optional(),
})
)
.action(async ({ parsedInput }): Promise<ServerActionResult<null>> => {
try {
const user = await (await auth.$context).internalAdapter.findUserByEmail(parsedInput.schema.email);
if (!user) {
return {
success: false,
actionError: {
message: "password_reset",
cause: "user_not_found",
},
};
}
const existingToken = await db.query.verification.findFirst({
where: (verifications, { eq, and, gte }) =>
and(eq(verifications.value, user.user.id), gte(verifications.expiresAt, new Date(Date.now() + 15 * 60 * 1000))),
});
if (existingToken) {
return {
success: false,
actionError: {
message: "password_reset",
cause: "reset_already_requested",
},
};
}
// await (
// await auth.$context
// ).options.emailAndPassword
// .sendResetPassword(
// {
// user: user.user,
// url,
// token: verificationToken,
// },
// ctx.request
// )
// .catch((e) => {
// ctx.context.logger.error("Failed to send reset password email", e);
// });
return {
success: true,
};
} catch (error) {
return {
success: false,
actionError: {
message: "password_reset",
cause: error instanceof Error ? error.message : "Unknown error",
},
};
}
});
+24
View File
@@ -0,0 +1,24 @@
"use client";
import { toast } from "sonner";
import { useRouter, useSearchParams } from "next/navigation";
import TwoFactorForm from "@/features/profile/2fa-form";
export const GuardForm = () => {
const router = useRouter();
const searchParams = useSearchParams();
const callbackUrl = searchParams.get("redirect") || "/dashboard";
return (
<TwoFactorForm
onSuccess={(success) => {
if (success) {
toast.success("Successfully logged in!");
//@ts-ignore
router.push(callbackUrl);
router.refresh();
}
}}
/>
);
};
+11
View File
@@ -0,0 +1,11 @@
"use client";
import { z } from "zod";
import { zEmail, zString } from "@/lib/zod";
export const LoginSchema = z.object({
email: zEmail(),
password: zString().nonempty(),
});
export type LoginType = z.infer<typeof LoginSchema>;
+133
View File
@@ -0,0 +1,133 @@
"use client";
import { useMutation } from "@tanstack/react-query";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect } from "react";
import { toast } from "sonner";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
useZodForm,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { PasswordInput } from "@/components/ui/password-input";
import {
LoginSchema,
type LoginType,
} from "@/features/auth/login-form.schema";
import { ButtonWithLoading } from "@/components/common/button-with-loading";
import { signIn } from "@/lib/auth/auth-client";
export type loginFormProps = {
defaultValues?: LoginType;
isPasskeyEnabled?: boolean;
};
export const LoginForm = (props: loginFormProps) => {
const { isPasskeyEnabled = false } = props;
const url = useSearchParams();
const form = useZodForm({
schema: LoginSchema,
});
const router = useRouter();
const mutation = useMutation({
mutationFn: async (values: LoginType) => {
await signIn.email(
{
password: values.password,
email: values.email,
callbackURL: url?.get("redirect") ?? "/dashboard",
},
{
onSuccess: (context) => {
if (context.data.twoFactorRedirect) {
router.push(
"/guard?redirect=" +
encodeURIComponent(context.data.callbackURL || "/dashboard"),
);
}
toast.success("Login success");
},
onError: (error) => {
toast.error(error.error.message);
},
},
);
},
});
return (
<Form
form={form}
className="flex flex-col gap-4 mb-1"
onSubmit={async (values) => {
await mutation.mutateAsync(values);
}}
>
<FormField
control={form.control}
name="email"
defaultValue=""
render={({ field }) => (
<FormItem>
<FormLabel>Email Address</FormLabel>
<FormControl>
<Input
autoComplete="email"
autoFocus
placeholder="example@portabase.io"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
defaultValue=""
render={({ field }) => (
<FormItem>
<div className="flex items-center justify-between">
<FormLabel>Password</FormLabel>
<div className="text-center text-sm">
<Link
href={"/forgot-password"}
className="hover:underline ml-1"
>
Forgot your password ?
</Link>
</div>
</div>
<FormControl>
<PasswordInput
autoComplete={
isPasskeyEnabled
? "current-password webauthn"
: "current-password"
}
placeholder={"Enter your password"}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<ButtonWithLoading className="mt-2 h-11" isPending={mutation.isPending}>
Login
</ButtonWithLoading>
</Form>
);
};
+25
View File
@@ -0,0 +1,25 @@
import { z } from "zod";
// Minimum 8 characters, at least one uppercase letter, one lowercase letter, one number and one special character
const passwordValidation = new RegExp(/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$/);
export const RegisterSchema = z
.object({
name: z.string(),
email: z.string(),
password: z.string().min(8, { message: "Must have at least 8 character" }).regex(passwordValidation, {
message: "Your password must contain at least one uppercase letter, one lowercase letter, one number, and one special character.",
}),
confirmPassword: z.string(),
})
.superRefine(({ confirmPassword, password }, ctx) => {
if (confirmPassword !== password) {
ctx.addIssue({
code: "custom",
message: "The passwords did not match",
path: ["confirmPassword"],
});
}
});
export type RegisterType = z.infer<typeof RegisterSchema>;
+169
View File
@@ -0,0 +1,169 @@
"use client";
import { useRouter } from "next/navigation";
import { useMutation } from "@tanstack/react-query";
import { toast } from "sonner";
import { CardContent, CardHeader } from "@/components/ui/card";
import { Form } from "@/components/ui/form";
import {
TooltipProvider,
TooltipTrigger,
Tooltip,
TooltipContent,
} from "@/components/ui/tooltip";
import {
RegisterSchema,
RegisterType,
} from "@/features/auth/register-form.schema";
import { signUp } from "@/lib/auth/auth-client";
import { useZodForm } from "@/components/ui/form";
import { CardAuth } from "@/features/layout/card-auth";
import { FormField } from "@/components/ui/form";
import { FormItem } from "@/components/ui/form";
import { FormLabel } from "@/components/ui/form";
import { FormControl } from "@/components/ui/form";
import { FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { PasswordInput } from "@/components/ui/password-input";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { Info } from "lucide-react";
export type registerFormProps = {
defaultValues?: RegisterType;
};
export const RegisterForm = (props: registerFormProps) => {
const form = useZodForm({
schema: RegisterSchema,
});
const router = useRouter();
const mutation = useMutation({
mutationFn: async (values: RegisterType) => {
await signUp.email(values, {
onSuccess: () => {
toast.success(`Account successfully created`);
router.refresh();
router.push(`/login`);
},
onError: (error) => {
toast.error(error.error.message);
},
});
},
});
return (
<TooltipProvider>
<CardAuth>
<CardHeader>
<div className="grid gap-2 text-center mb-2">
<h1 className="text-3xl font-bold">Create an account</h1>
<p className="text-balance text-muted-foreground">
Enter your informations below to register
</p>
</div>
</CardHeader>
<CardContent>
<Form
form={form}
className="flex flex-col gap-4"
onSubmit={async (values) => {
await mutation.mutateAsync(values);
}}
>
<FormField
control={form.control}
name="name"
defaultValue=""
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Your name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
defaultValue=""
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="example@portabase.io" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
defaultValue=""
render={({ field }) => (
<FormItem>
<FormLabel className="flex">
Password
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info size="15" />
</TooltipTrigger>
<TooltipContent>
<p>
Min. 8 characters, 1 uppercase (A-Z), 1 lowercase
(a-z), 1 number (0-9), 1 special character (!, @,
etc.)
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<FormControl>
<PasswordInput placeholder="Your password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
defaultValue=""
render={({ field }) => (
<FormItem>
<FormLabel>Password Confirmation</FormLabel>
<FormControl>
<PasswordInput
placeholder="Conform your password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
className="h-11"
disabled={mutation.isPending}
>
Sign up
</Button>
<div className="mt-4 text-center text-sm">
Already have an account ?{" "}
<Link href="/login" className="underline">
Sign in
</Link>
</div>
</Form>
</CardContent>
</CardAuth>
</TooltipProvider>
);
};
@@ -0,0 +1,21 @@
"use client";
import {z} from "zod";
import {zPassword} from "@/lib/zod";
export const ResetPasswordSchema = z
.object({
password: zPassword(),
confirmPassword: zPassword(),
})
.superRefine(({confirmPassword, password}, ctx) => {
if (confirmPassword !== password) {
ctx.addIssue({
code: "custom",
message: "Confirmation password does not match",
path: ["confirmPassword"],
});
}
});
export type ResetPasswordType = z.infer<typeof ResetPasswordSchema>;
+96
View File
@@ -0,0 +1,96 @@
"use client";
import {useMutation} from "@tanstack/react-query";
import {toast} from "sonner";
import {FormControl, FormField, FormItem, FormLabel, useZodForm} from "@/components/ui/form";
import {Form} from "@/components/ui/form";
import {ButtonWithLoading} from "@/components/common/button-with-loading";
import Link from "next/link";
import {ResetPasswordSchema, ResetPasswordType} from "./reset-password-form.schema";
import {PasswordStrengthInput} from "@/components/ui/password-input-indicator";
import {useRouter, useSearchParams} from "next/navigation";
import {ArrowLeft} from "lucide-react";
import {authClient} from "@/lib/auth/auth-client";
import {BetterAuthError} from "@/types/auth";
import {PasswordInput} from "@/components/ui/password-input";
export type ResetPasswordFormProps = {
defaultValues?: ResetPasswordType;
};
export const ResetPasswordForm = (props: ResetPasswordFormProps) => {
const searchParams = useSearchParams();
const form = useZodForm({
schema: ResetPasswordSchema,
});
const router = useRouter();
const mutation = useMutation({
mutationFn: async (values: ResetPasswordType) => {
const {data, error} = await authClient.resetPassword({
newPassword: values.password,
token: searchParams.get("token") || "",
});
if (error) throw error;
},
onSuccess: () => {
toast.success("Password successfully reset!");
setTimeout(() => router.push("/"), 1400);
},
onError: (error: BetterAuthError) => {
toast.error("An error occurred while resetting password");
},
});
return (
<Form
form={form}
className="flex flex-col gap-4 mb-1"
onSubmit={async (values) => {
await mutation.mutateAsync(values);
}}
>
<FormField
control={form.control}
name="password"
defaultValue=""
render={({field}) => (
<FormItem>
<PasswordStrengthInput label={"New password"} field={field}/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
defaultValue=""
render={({field}) => (
<FormItem>
<FormLabel>Confirmation password</FormLabel>
<FormControl>
<PasswordInput placeholder={"Enter your conformation password"} {...field} />
</FormControl>
</FormItem>
)}
/>
<div className="flex flex-col items-center gap-y-6 w-full">
<ButtonWithLoading className="mt-2 w-full h-11" isPending={mutation.isPending}>
Reset
</ButtonWithLoading>
<Link href="/login" className="group flex items-center text-sm hover:underline">
<ArrowLeft
className="mr-1 size-4 text-muted-foreground transition-transform group-hover:-translate-x-1"/>
Back to login
</Link>
</div>
</Form>
);
};
@@ -0,0 +1,19 @@
import {z} from "zod";
import {zPassword} from "@/lib/zod";
export const ResetPasswordSchema = z
.object({
password: zPassword(),
confirmPassword: zPassword(),
})
.superRefine(({confirmPassword, password}, ctx) => {
if (confirmPassword !== password) {
ctx.addIssue({
code: "custom",
message: "Passwords don't match.",
path: ["confirmPassword"],
});
}
});
export type ResetPasswordType = z.infer<typeof ResetPasswordSchema>;
@@ -0,0 +1,62 @@
"use server";
import { ServerActionResult } from "@/types/action-type";
import { auth } from "@/lib/auth/auth";
import { zPassword, zString } from "@/lib/zod";
import z from "zod";
import {action} from "@/lib/safe-actions/actions";
export const resetPasswordAction = action
.schema(
z.object({
schema: z.object({
password: zPassword(),
}),
token: zString(),
})
)
.action(async ({ parsedInput }): Promise<ServerActionResult<null>> => {
try {
const verification = await (await auth.$context).internalAdapter.findVerificationValue(`reset-password:${parsedInput.token}`);
if (!verification || verification.expiresAt < new Date()) {
return {
success: false,
actionError: {
message: "password_reset",
cause: "invalid_or_expired_token",
},
};
}
const user = await (await auth.$context).internalAdapter.findUserById(verification.value);
if (!user) {
return {
success: false,
actionError: {
message: "password_reset",
cause: "user_not_found",
},
};
}
await (
await auth.$context
).internalAdapter.updateUser(user.id, {
isDefaultPassword: false,
});
return {
success: true,
actionSuccess: {
message: "password_reset",
},
};
} catch (error) {
return {
success: false,
actionError: {
message: "password_reset",
cause: error instanceof Error ? error.message : "Unknown error",
},
};
}
});
+86
View File
@@ -0,0 +1,86 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { authClient, passkey } from "@/lib/auth/auth-client";
import { Loader2 } from "lucide-react";
import { toast } from "sonner";
import { Icon } from "@iconify/react";
import { AuthProviderConfig } from "@/lib/auth/config";
import Image from "next/image";
import { useRouter } from "next/navigation";
export function SocialAuthButtons({ providers }: { providers: AuthProviderConfig[] }) {
const socialProviders = providers.filter((p) => p.isActive && p.type !== "credential");
const router = useRouter();
const [isLoading, setIsLoading] = useState<string | null>(null);
const handleSocialSignIn = async (provider: AuthProviderConfig) => {
setIsLoading(provider.id);
try {
let result;
if (provider.id === "passkey") {
result = await authClient.signIn.passkey({
fetchOptions: {
onSuccess() {
router.push("/dashboard");
},
onError() {
toast.error("An error occurred during passkey authentication. Please try again.");
},
},
});
} else if (provider.type === "sso") {
result = await authClient.signIn.sso({
providerId: provider.id,
providerType: "oidc",
callbackURL: "/dashboard",
});
console.log(result);
} else {
result = await authClient.signIn.social({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
provider: provider.id as any,
callbackURL: "/dashboard",
});
}
if (result?.error) {
toast.error("An error occurred while signing in with the provider. Please try again.");
} else if (provider.id !== "passkey") {
toast.success("Redirecting to provider...");
}
} catch (err) {
toast.error("An error occurred while signing in with the provider. Please try again.");
} finally {
setIsLoading(null);
}
};
if (socialProviders.length === 0) return null;
return (
<div className="flex flex-col gap-2 w-full">
{socialProviders.map((provider) => (
<Button key={provider.id} variant="outline" className="w-full gap-2" onClick={() => handleSocialSignIn(provider)} disabled={!!isLoading}>
{isLoading === provider.id ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : provider.icon.startsWith("/") || provider.icon.startsWith("http") ? (
<Image
src={provider.icon}
alt={provider.id || "icon"}
width={16}
height={16}
className="h-4 w-4"
unoptimized={provider.icon.startsWith("http")}
/>
) : (
<Icon icon={provider.icon} className="h-4 w-4" />
)}
<span>{provider.title || provider.name}</span>
</Button>
))}
</div>
);
}