* feat: setup better-auth with 2fa * feat(totp): frontend * refactor: split dialogs into components * feat: disable 2fa cli * chore: fix liniting issues * chore(deps): bump the minor-patch group across 1 directory with 19 updates (#327) * chore(deps): bump the minor-patch group across 1 directory with 19 updates Bumps the minor-patch group with 19 updates in the / directory: | Package | From | To | | --- | --- | --- | | [@react-router/node](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-node) | `7.11.0` | `7.12.0` | | [@react-router/serve](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-serve) | `7.11.0` | `7.12.0` | | [@scalar/hono-api-reference](https://github.com/scalar/scalar/tree/HEAD/integrations/hono) | `0.9.30` | `0.9.32` | | [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) | `5.90.12` | `5.90.16` | | [drizzle-orm](https://github.com/drizzle-team/drizzle-orm) | `0.44.7` | `0.45.1` | | [hono](https://github.com/honojs/hono) | `4.10.5` | `4.11.3` | | [hono-rate-limiter](https://github.com/rhinobase/hono-rate-limiter) | `0.5.1` | `0.5.3` | | [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.555.0` | `0.562.0` | | [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.69.0` | `7.70.0` | | [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) | `7.11.0` | `7.12.0` | | [recharts](https://github.com/recharts/recharts) | `3.5.1` | `3.6.0` | | [@faker-js/faker](https://github.com/faker-js/faker) | `10.1.0` | `10.2.0` | | [@happy-dom/global-registrator](https://github.com/capricorn86/happy-dom) | `20.0.11` | `20.1.0` | | [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts) | `0.88.2` | `0.90.2` | | [@react-router/dev](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dev) | `7.11.0` | `7.12.0` | | [@tanstack/react-query-devtools](https://github.com/TanStack/query/tree/HEAD/packages/react-query-devtools) | `5.91.1` | `5.91.2` | | [oxfmt](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxfmt) | `0.22.0` | `0.23.0` | | [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint) | `1.36.0` | `1.38.0` | | [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `7.3.0` | `7.3.1` | Updates `@react-router/node` from 7.11.0 to 7.12.0 - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-node/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/@react-router/node@7.12.0/packages/react-router-node) Updates `@react-router/serve` from 7.11.0 to 7.12.0 - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-serve/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/@react-router/serve@7.12.0/packages/react-router-serve) Updates `@scalar/hono-api-reference` from 0.9.30 to 0.9.32 - [Release notes](https://github.com/scalar/scalar/releases) - [Changelog](https://github.com/scalar/scalar/blob/main/integrations/hono/CHANGELOG.md) - [Commits](https://github.com/scalar/scalar/commits/HEAD/integrations/hono) Updates `@tanstack/react-query` from 5.90.12 to 5.90.16 - [Release notes](https://github.com/TanStack/query/releases) - [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md) - [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.90.16/packages/react-query) Updates `drizzle-orm` from 0.44.7 to 0.45.1 - [Release notes](https://github.com/drizzle-team/drizzle-orm/releases) - [Commits](https://github.com/drizzle-team/drizzle-orm/compare/0.44.7...0.45.1) Updates `hono` from 4.10.5 to 4.11.3 - [Release notes](https://github.com/honojs/hono/releases) - [Commits](https://github.com/honojs/hono/compare/v4.10.5...v4.11.3) Updates `hono-rate-limiter` from 0.5.1 to 0.5.3 - [Release notes](https://github.com/rhinobase/hono-rate-limiter/releases) - [Commits](https://github.com/rhinobase/hono-rate-limiter/compare/v0.5.1...v0.5.3) Updates `lucide-react` from 0.555.0 to 0.562.0 - [Release notes](https://github.com/lucide-icons/lucide/releases) - [Commits](https://github.com/lucide-icons/lucide/commits/0.562.0/packages/lucide-react) Updates `react-hook-form` from 7.69.0 to 7.70.0 - [Release notes](https://github.com/react-hook-form/react-hook-form/releases) - [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md) - [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.69.0...v7.70.0) Updates `react-router` from 7.11.0 to 7.12.0 - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/react-router@7.12.0/packages/react-router) Updates `recharts` from 3.5.1 to 3.6.0 - [Release notes](https://github.com/recharts/recharts/releases) - [Changelog](https://github.com/recharts/recharts/blob/main/CHANGELOG.md) - [Commits](https://github.com/recharts/recharts/compare/v3.5.1...v3.6.0) Updates `@faker-js/faker` from 10.1.0 to 10.2.0 - [Release notes](https://github.com/faker-js/faker/releases) - [Changelog](https://github.com/faker-js/faker/blob/next/CHANGELOG.md) - [Commits](https://github.com/faker-js/faker/compare/v10.1.0...v10.2.0) Updates `@happy-dom/global-registrator` from 20.0.11 to 20.1.0 - [Release notes](https://github.com/capricorn86/happy-dom/releases) - [Commits](https://github.com/capricorn86/happy-dom/compare/v20.0.11...v20.1.0) Updates `@hey-api/openapi-ts` from 0.88.2 to 0.90.2 - [Release notes](https://github.com/hey-api/openapi-ts/releases) - [Changelog](https://github.com/hey-api/openapi-ts/blob/main/docs/CHANGELOG.md) - [Commits](https://github.com/hey-api/openapi-ts/compare/@hey-api/openapi-ts@0.88.2...@hey-api/openapi-ts@0.90.2) Updates `@react-router/dev` from 7.11.0 to 7.12.0 - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dev/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/@react-router/dev@7.12.0/packages/react-router-dev) Updates `@tanstack/react-query-devtools` from 5.91.1 to 5.91.2 - [Release notes](https://github.com/TanStack/query/releases) - [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query-devtools/CHANGELOG.md) - [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query-devtools@5.91.2/packages/react-query-devtools) Updates `oxfmt` from 0.22.0 to 0.23.0 - [Release notes](https://github.com/oxc-project/oxc/releases) - [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxfmt/CHANGELOG.md) - [Commits](https://github.com/oxc-project/oxc/commits/oxfmt_v0.23.0/npm/oxfmt) Updates `oxlint` from 1.36.0 to 1.38.0 - [Release notes](https://github.com/oxc-project/oxc/releases) - [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxlint/CHANGELOG.md) - [Commits](https://github.com/oxc-project/oxc/commits/oxlint_v1.38.0/npm/oxlint) Updates `vite` from 7.3.0 to 7.3.1 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v7.3.1/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v7.3.1/packages/vite) --- updated-dependencies: - dependency-name: "@react-router/node" dependency-version: 7.12.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@react-router/serve" dependency-version: 7.12.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@scalar/hono-api-reference" dependency-version: 0.9.32 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: "@tanstack/react-query" dependency-version: 5.90.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: drizzle-orm dependency-version: 0.45.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: hono dependency-version: 4.11.3 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: hono-rate-limiter dependency-version: 0.5.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: lucide-react dependency-version: 0.562.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: react-hook-form dependency-version: 7.70.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: react-router dependency-version: 7.12.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: recharts dependency-version: 3.6.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@faker-js/faker" dependency-version: 10.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@happy-dom/global-registrator" dependency-version: 20.1.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@hey-api/openapi-ts" dependency-version: 0.90.2 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@react-router/dev" dependency-version: 7.12.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@tanstack/react-query-devtools" dependency-version: 5.91.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: oxfmt dependency-version: 0.23.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: oxlint dependency-version: 1.38.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: vite dependency-version: 7.3.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch ... Signed-off-by: dependabot[bot] <support@github.com> * chore: downgrade hono --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Nicolas Meienberger <github@thisprops.com> * chore: force hono version in transitive deps * refactor: remove copy to clipboard everywhere as it's not possible on http * chore: pr feedbacks --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
246 lines
6.6 KiB
TypeScript
246 lines
6.6 KiB
TypeScript
import { arktypeResolver } from "@hookform/resolvers/arktype";
|
|
import { type } from "arktype";
|
|
import { useState } from "react";
|
|
import { useForm } from "react-hook-form";
|
|
import { useNavigate } from "react-router";
|
|
import { toast } from "sonner";
|
|
import { AuthLayout } from "~/client/components/auth-layout";
|
|
import { Button } from "~/client/components/ui/button";
|
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form";
|
|
import { Input } from "~/client/components/ui/input";
|
|
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "~/client/components/ui/input-otp";
|
|
import { Label } from "~/client/components/ui/label";
|
|
import { authClient } from "~/client/lib/auth-client";
|
|
import { authMiddleware } from "~/middleware/auth";
|
|
import { ResetPasswordDialog } from "../components/reset-password-dialog";
|
|
import type { Route } from "./+types/login";
|
|
|
|
export const clientMiddleware = [authMiddleware];
|
|
|
|
export function meta(_: Route.MetaArgs) {
|
|
return [
|
|
{ title: "Zerobyte - Login" },
|
|
{
|
|
name: "description",
|
|
content: "Sign in to your Zerobyte account.",
|
|
},
|
|
];
|
|
}
|
|
|
|
const loginSchema = type({
|
|
username: "2<=string<=50",
|
|
password: "string>=1",
|
|
});
|
|
|
|
type LoginFormValues = typeof loginSchema.inferIn;
|
|
|
|
export default function LoginPage() {
|
|
const navigate = useNavigate();
|
|
const [showResetDialog, setShowResetDialog] = useState(false);
|
|
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
|
const [requires2FA, setRequires2FA] = useState(false);
|
|
const [totpCode, setTotpCode] = useState("");
|
|
const [isVerifying2FA, setIsVerifying2FA] = useState(false);
|
|
const [trustDevice, setTrustDevice] = useState(false);
|
|
|
|
const form = useForm<LoginFormValues>({
|
|
resolver: arktypeResolver(loginSchema),
|
|
defaultValues: {
|
|
username: "",
|
|
password: "",
|
|
},
|
|
});
|
|
|
|
const onSubmit = async (values: LoginFormValues) => {
|
|
const { data, error } = await authClient.signIn.username({
|
|
username: values.username.toLowerCase().trim(),
|
|
password: values.password,
|
|
fetchOptions: {
|
|
onRequest: () => {
|
|
setIsLoggingIn(true);
|
|
},
|
|
onResponse: () => {
|
|
setIsLoggingIn(false);
|
|
},
|
|
},
|
|
});
|
|
|
|
if (error) {
|
|
console.error(error);
|
|
toast.error("Login failed", { description: error.message });
|
|
return;
|
|
}
|
|
|
|
if ("twoFactorRedirect" in data && data.twoFactorRedirect) {
|
|
setRequires2FA(true);
|
|
return;
|
|
}
|
|
|
|
const d = await authClient.getSession();
|
|
if (data.user && !d.data?.user.hasDownloadedResticPassword) {
|
|
void navigate("/download-recovery-key");
|
|
} else {
|
|
void navigate("/volumes");
|
|
}
|
|
};
|
|
|
|
const handleVerify2FA = async () => {
|
|
if (totpCode.length !== 6) {
|
|
toast.error("Please enter a 6-digit code");
|
|
return;
|
|
}
|
|
|
|
const { data, error } = await authClient.twoFactor.verifyTotp({
|
|
code: totpCode,
|
|
trustDevice,
|
|
fetchOptions: {
|
|
onRequest: () => {
|
|
setIsVerifying2FA(true);
|
|
},
|
|
onResponse: () => {
|
|
setIsVerifying2FA(false);
|
|
},
|
|
},
|
|
});
|
|
|
|
if (error) {
|
|
console.error(error);
|
|
toast.error("Verification failed", { description: error.message });
|
|
setTotpCode("");
|
|
return;
|
|
}
|
|
|
|
if (data) {
|
|
toast.success("Login successful");
|
|
const session = await authClient.getSession();
|
|
if (session.data?.user && !session.data.user.hasDownloadedResticPassword) {
|
|
void navigate("/download-recovery-key");
|
|
} else {
|
|
void navigate("/volumes");
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleBackToLogin = () => {
|
|
setRequires2FA(false);
|
|
setTotpCode("");
|
|
setTrustDevice(false);
|
|
form.reset();
|
|
};
|
|
|
|
if (requires2FA) {
|
|
return (
|
|
<AuthLayout title="Two-Factor Authentication" description="Enter the 6-digit code from your authenticator app">
|
|
<div className="space-y-6">
|
|
<div className="space-y-4 flex flex-col items-center">
|
|
<Label htmlFor="totp-code">Authentication code</Label>
|
|
<div>
|
|
<InputOTP
|
|
maxLength={6}
|
|
value={totpCode}
|
|
onChange={setTotpCode}
|
|
onComplete={handleVerify2FA}
|
|
disabled={isVerifying2FA}
|
|
>
|
|
<InputOTPGroup>
|
|
<InputOTPSlot index={0} />
|
|
<InputOTPSlot index={1} />
|
|
<InputOTPSlot index={2} />
|
|
</InputOTPGroup>
|
|
<InputOTPSeparator />
|
|
<InputOTPGroup>
|
|
<InputOTPSlot index={3} />
|
|
<InputOTPSlot index={4} />
|
|
<InputOTPSlot index={5} />
|
|
</InputOTPGroup>
|
|
</InputOTP>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<input
|
|
type="checkbox"
|
|
id="trust-device"
|
|
checked={trustDevice}
|
|
onChange={(e) => setTrustDevice(e.target.checked)}
|
|
className="h-4 w-4"
|
|
/>
|
|
<label htmlFor="trust-device" className="text-sm text-muted-foreground cursor-pointer">
|
|
Trust this device for 30 days
|
|
</label>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Button
|
|
type="button"
|
|
className="w-full"
|
|
loading={isVerifying2FA}
|
|
onClick={handleVerify2FA}
|
|
disabled={totpCode.length !== 6}
|
|
>
|
|
Verify
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
className="w-full"
|
|
onClick={handleBackToLogin}
|
|
disabled={isVerifying2FA}
|
|
>
|
|
Back to Login
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</AuthLayout>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<AuthLayout title="Login to your account" description="Enter your credentials below to login to your account">
|
|
<Form {...form}>
|
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
<FormField
|
|
control={form.control}
|
|
name="username"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Username</FormLabel>
|
|
<FormControl>
|
|
<Input {...field} type="text" placeholder="admin" disabled={isLoggingIn} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="password"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<div className="flex items-center justify-between">
|
|
<FormLabel>Password</FormLabel>
|
|
<button
|
|
type="button"
|
|
className="text-xs text-muted-foreground hover:underline"
|
|
onClick={() => setShowResetDialog(true)}
|
|
>
|
|
Forgot your password?
|
|
</button>
|
|
</div>
|
|
<FormControl>
|
|
<Input {...field} type="password" disabled={isLoggingIn} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<Button type="submit" className="w-full" loading={isLoggingIn}>
|
|
Login
|
|
</Button>
|
|
</form>
|
|
</Form>
|
|
|
|
<ResetPasswordDialog open={showResetDialog} onOpenChange={setShowResetDialog} />
|
|
</AuthLayout>
|
|
);
|
|
}
|