feat: setup better-auth with 2fa
This commit is contained in:
parent
d0dd74a626
commit
52ed37481b
10 changed files with 1450 additions and 134 deletions
1
.github/workflows/release.yml
vendored
1
.github/workflows/release.yml
vendored
|
|
@ -84,6 +84,7 @@ jobs:
|
|||
with:
|
||||
image: local/zerobyte:ci
|
||||
fail-build: true
|
||||
only-fixed: true
|
||||
severity-cutoff: critical
|
||||
|
||||
- name: upload Anchore scan report
|
||||
|
|
|
|||
75
app/client/components/ui/input-otp.tsx
Normal file
75
app/client/components/ui/input-otp.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import * as React from "react"
|
||||
import { OTPInput, OTPInputContext } from "input-otp"
|
||||
import { MinusIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "~/client/lib/utils"
|
||||
|
||||
function InputOTP({
|
||||
className,
|
||||
containerClassName,
|
||||
...props
|
||||
}: React.ComponentProps<typeof OTPInput> & {
|
||||
containerClassName?: string
|
||||
}) {
|
||||
return (
|
||||
<OTPInput
|
||||
data-slot="input-otp"
|
||||
containerClassName={cn(
|
||||
"flex items-center gap-2 has-disabled:opacity-50",
|
||||
containerClassName
|
||||
)}
|
||||
className={cn("disabled:cursor-not-allowed", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="input-otp-group"
|
||||
className={cn("flex items-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function InputOTPSlot({
|
||||
index,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
index: number
|
||||
}) {
|
||||
const inputOTPContext = React.useContext(OTPInputContext)
|
||||
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="input-otp-slot"
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{char}
|
||||
{hasFakeCaret && (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div data-slot="input-otp-separator" role="separator" {...props}>
|
||||
<MinusIcon />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { createAuthClient } from "better-auth/react";
|
||||
import { usernameClient } from "better-auth/client/plugins";
|
||||
import { twoFactorClient, usernameClient } from "better-auth/client/plugins";
|
||||
import { inferAdditionalFields } from "better-auth/client/plugins";
|
||||
import type { auth } from "~/lib/auth";
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
plugins: [inferAdditionalFields<typeof auth>(), usernameClient()],
|
||||
plugins: [inferAdditionalFields<typeof auth>(), usernameClient(), twoFactorClient()],
|
||||
});
|
||||
|
|
|
|||
12
app/drizzle/0031_graceful_squadron_supreme.sql
Normal file
12
app/drizzle/0031_graceful_squadron_supreme.sql
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
CREATE TABLE `two_factor` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`secret` text NOT NULL,
|
||||
`backup_codes` text NOT NULL,
|
||||
`user_id` text NOT NULL,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users_table`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `twoFactor_secret_idx` ON `two_factor` (`secret`);--> statement-breakpoint
|
||||
CREATE INDEX `twoFactor_userId_idx` ON `two_factor` (`user_id`);--> statement-breakpoint
|
||||
ALTER TABLE `users_table` ADD `two_factor_enabled` integer DEFAULT false NOT NULL;--> statement-breakpoint
|
||||
CREATE INDEX `sessionsTable_userId_idx` ON `sessions_table` (`user_id`);
|
||||
1195
app/drizzle/meta/0031_snapshot.json
Normal file
1195
app/drizzle/meta/0031_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -218,6 +218,13 @@
|
|||
"when": 1767821088612,
|
||||
"tag": "0030_lower-trim-username",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 31,
|
||||
"version": "6",
|
||||
"when": 1767863951955,
|
||||
"tag": "0031_graceful_squadron_supreme",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ import {
|
|||
type MiddlewareOptions,
|
||||
} from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { createAuthMiddleware, username } from "better-auth/plugins";
|
||||
import { createAuthMiddleware, twoFactor, username } from "better-auth/plugins";
|
||||
import { convertLegacyUserOnFirstLogin } from "./auth-middlewares/convert-legacy-user";
|
||||
import { cryptoUtils } from "~/server/utils/crypto";
|
||||
import { db } from "~/server/db/db";
|
||||
|
|
|
|||
|
|
@ -50,26 +50,31 @@ export const usersTable = sqliteTable("users_table", {
|
|||
emailVerified: integer("email_verified", { mode: "boolean" }).default(false).notNull(),
|
||||
image: text("image"),
|
||||
displayUsername: text("display_username"),
|
||||
twoFactorEnabled: integer("two_factor_enabled", { mode: "boolean" }).notNull().default(false),
|
||||
});
|
||||
|
||||
export type User = typeof usersTable.$inferSelect;
|
||||
export const sessionsTable = sqliteTable("sessions_table", {
|
||||
id: text().primaryKey(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => usersTable.id, { onDelete: "cascade" }),
|
||||
token: text("token").notNull().unique(),
|
||||
expiresAt: int("expires_at", { mode: "timestamp_ms" }).notNull(),
|
||||
createdAt: int("created_at", { mode: "timestamp_ms" })
|
||||
.notNull()
|
||||
.default(sql`(unixepoch() * 1000)`),
|
||||
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
||||
.notNull()
|
||||
.$onUpdate(() => new Date())
|
||||
.default(sql`(unixepoch() * 1000)`),
|
||||
ipAddress: text("ip_address"),
|
||||
userAgent: text("user_agent"),
|
||||
});
|
||||
export const sessionsTable = sqliteTable(
|
||||
"sessions_table",
|
||||
{
|
||||
id: text().primaryKey(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => usersTable.id, { onDelete: "cascade" }),
|
||||
token: text("token").notNull().unique(),
|
||||
expiresAt: int("expires_at", { mode: "timestamp_ms" }).notNull(),
|
||||
createdAt: int("created_at", { mode: "timestamp_ms" })
|
||||
.notNull()
|
||||
.default(sql`(unixepoch() * 1000)`),
|
||||
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
||||
.notNull()
|
||||
.$onUpdate(() => new Date())
|
||||
.default(sql`(unixepoch() * 1000)`),
|
||||
ipAddress: text("ip_address"),
|
||||
userAgent: text("user_agent"),
|
||||
},
|
||||
(table) => [index("sessionsTable_userId_idx").on(table.userId)],
|
||||
);
|
||||
export type Session = typeof sessionsTable.$inferSelect;
|
||||
|
||||
export const account = sqliteTable(
|
||||
|
|
@ -124,6 +129,7 @@ export const verification = sqliteTable(
|
|||
export const userRelations = relations(usersTable, ({ many }) => ({
|
||||
sessions: many(sessionsTable),
|
||||
accounts: many(account),
|
||||
twoFactors: many(twoFactor),
|
||||
}));
|
||||
|
||||
export const sessionRelations = relations(sessionsTable, ({ one }) => ({
|
||||
|
|
@ -326,3 +332,16 @@ export const appMetadataTable = sqliteTable("app_metadata", {
|
|||
.default(sql`(unixepoch() * 1000)`),
|
||||
});
|
||||
export type AppMetadata = typeof appMetadataTable.$inferSelect;
|
||||
|
||||
export const twoFactor = sqliteTable(
|
||||
"two_factor",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
secret: text("secret").notNull(),
|
||||
backupCodes: text("backup_codes").notNull(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => usersTable.id, { onDelete: "cascade" }),
|
||||
},
|
||||
(table) => [index("twoFactor_secret_idx").on(table.secret), index("twoFactor_userId_idx").on(table.userId)],
|
||||
);
|
||||
|
|
|
|||
6
bun.lock
6
bun.lock
|
|
@ -44,10 +44,12 @@
|
|||
"hono-openapi": "^1.1.1",
|
||||
"hono-rate-limiter": "^0.5.3",
|
||||
"http-errors-enhanced": "^4.0.2",
|
||||
"input-otp": "^1.4.2",
|
||||
"isbot": "^5.1.32",
|
||||
"lucide-react": "^0.562.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"node-cron": "^4.2.1",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"react-hook-form": "^7.70.0",
|
||||
|
|
@ -956,6 +958,8 @@
|
|||
|
||||
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
|
||||
|
||||
"input-otp": ["input-otp@1.4.2", "", { "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA=="],
|
||||
|
||||
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
|
@ -1260,6 +1264,8 @@
|
|||
|
||||
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
|
||||
|
||||
"qrcode.react": ["qrcode.react@4.2.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA=="],
|
||||
|
||||
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
|
||||
|
||||
"quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],
|
||||
|
|
|
|||
229
package.json
229
package.json
|
|
@ -1,116 +1,117 @@
|
|||
{
|
||||
"name": "zerobyte",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"lint": "oxlint --type-aware",
|
||||
"build": "react-router build",
|
||||
"dev": "bunx --bun vite",
|
||||
"start": "bun ./dist/server/index.js",
|
||||
"cli:dev": "bun run app/server/cli/main.ts",
|
||||
"cli": "ZEROBYTE_CLI=1 bun ./dist/server/index.js",
|
||||
"tsc": "react-router typegen && tsc",
|
||||
"start:dev": "docker compose down && docker compose up --build zerobyte-dev",
|
||||
"start:prod": "docker compose down && docker compose up --build zerobyte-prod",
|
||||
"gen:api-client": "openapi-ts",
|
||||
"gen:migrations": "drizzle-kit generate",
|
||||
"studio": "drizzle-kit studio",
|
||||
"test:server": "dotenv -e .env.test -- bun test app/server --preload ./app/test/setup.ts",
|
||||
"test:client": "dotenv -e .env.test -- bun test app/client --preload ./app/test/setup-client.ts",
|
||||
"test": "bun run test:server && bun run test:client"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@hono/standard-validator": "^0.2.0",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@inquirer/prompts": "^8.0.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-progress": "^1.1.8",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@react-router/node": "^7.12.0",
|
||||
"@react-router/serve": "^7.12.0",
|
||||
"@scalar/hono-api-reference": "^0.9.32",
|
||||
"@tanstack/react-query": "^5.90.16",
|
||||
"arktype": "^2.1.28",
|
||||
"better-auth": "^1.4.10",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"commander": "^14.0.2",
|
||||
"cron-parser": "^5.4.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"dither-plugin": "^1.1.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"es-toolkit": "^1.42.0",
|
||||
"hono": "^4.11.3",
|
||||
"hono-openapi": "^1.1.1",
|
||||
"hono-rate-limiter": "^0.5.3",
|
||||
"http-errors-enhanced": "^4.0.2",
|
||||
"isbot": "^5.1.32",
|
||||
"lucide-react": "^0.562.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"node-cron": "^4.2.1",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"react-hook-form": "^7.70.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router": "^7.12.0",
|
||||
"react-router-hono-server": "^2.22.0",
|
||||
"recharts": "3.6.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"semver": "^7.7.3",
|
||||
"slugify": "^1.6.6",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tiny-typed-emitter": "^2.1.0",
|
||||
"winston": "^3.18.3",
|
||||
"yaml": "^2.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^10.2.0",
|
||||
"@happy-dom/global-registrator": "^20.1.0",
|
||||
"@hey-api/openapi-ts": "^0.90.2",
|
||||
"@react-router/dev": "^7.12.0",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"@tanstack/react-query-devtools": "^5.91.2",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/react": "^16.3.1",
|
||||
"@types/bun": "^1.3.4",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/semver": "^7.7.1",
|
||||
"dotenv-cli": "^11.0.0",
|
||||
"drizzle-kit": "^0.31.7",
|
||||
"lightningcss": "^1.30.2",
|
||||
"oxfmt": "^0.23.0",
|
||||
"oxlint": "^1.38.0",
|
||||
"oxlint-tsgolint": "^0.10.1",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
"vite-bundle-analyzer": "^1.2.3",
|
||||
"vite-tsconfig-paths": "^6.0.3"
|
||||
},
|
||||
"overrides": {
|
||||
"esbuild": "^0.27.2",
|
||||
"hono": "^4.11.3"
|
||||
},
|
||||
"packageManager": "bun@1.3.5"
|
||||
"name": "zerobyte",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.3.5",
|
||||
"scripts": {
|
||||
"lint": "oxlint --type-aware",
|
||||
"build": "react-router build",
|
||||
"dev": "bunx --bun vite",
|
||||
"start": "bun ./dist/server/index.js",
|
||||
"cli:dev": "bun run app/server/cli/main.ts",
|
||||
"cli": "ZEROBYTE_CLI=1 bun ./dist/server/index.js",
|
||||
"tsc": "react-router typegen && tsc",
|
||||
"start:dev": "docker compose down && docker compose up --build zerobyte-dev",
|
||||
"start:prod": "docker compose down && docker compose up --build zerobyte-prod",
|
||||
"gen:api-client": "openapi-ts",
|
||||
"gen:migrations": "drizzle-kit generate",
|
||||
"studio": "drizzle-kit studio",
|
||||
"test:server": "dotenv -e .env.test -- bun test app/server --preload ./app/test/setup.ts",
|
||||
"test:client": "dotenv -e .env.test -- bun test app/client --preload ./app/test/setup-client.ts",
|
||||
"test": "bun run test:server && bun run test:client"
|
||||
},
|
||||
"overrides": {
|
||||
"esbuild": "^0.27.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@hono/standard-validator": "^0.2.0",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@inquirer/prompts": "^8.0.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-progress": "^1.1.8",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@react-router/node": "^7.10.0",
|
||||
"@react-router/serve": "^7.10.0",
|
||||
"@scalar/hono-api-reference": "^0.9.25",
|
||||
"@tanstack/react-query": "^5.90.11",
|
||||
"arktype": "^2.1.28",
|
||||
"better-auth": "^1.4.10",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"commander": "^14.0.2",
|
||||
"cron-parser": "^5.4.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"dither-plugin": "^1.1.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"drizzle-orm": "^0.44.7",
|
||||
"es-toolkit": "^1.42.0",
|
||||
"hono": "4.10.5",
|
||||
"hono-openapi": "^1.1.1",
|
||||
"hono-rate-limiter": "^0.5.0",
|
||||
"http-errors-enhanced": "^4.0.2",
|
||||
"input-otp": "^1.4.2",
|
||||
"isbot": "^5.1.32",
|
||||
"lucide-react": "^0.555.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"node-cron": "^4.2.1",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"react-hook-form": "^7.68.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router": "^7.10.0",
|
||||
"react-router-hono-server": "^2.22.0",
|
||||
"recharts": "3.5.1",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"semver": "^7.7.3",
|
||||
"slugify": "^1.6.6",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tiny-typed-emitter": "^2.1.0",
|
||||
"winston": "^3.18.3",
|
||||
"yaml": "^2.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^10.1.0",
|
||||
"@happy-dom/global-registrator": "^20.0.11",
|
||||
"@hey-api/openapi-ts": "^0.88.0",
|
||||
"@react-router/dev": "^7.10.0",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"@tanstack/react-query-devtools": "^5.91.1",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/react": "^16.3.1",
|
||||
"@types/bun": "^1.3.4",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/semver": "^7.7.1",
|
||||
"dotenv-cli": "^11.0.0",
|
||||
"drizzle-kit": "^0.31.7",
|
||||
"lightningcss": "^1.30.2",
|
||||
"oxfmt": "^0.22.0",
|
||||
"oxlint": "^1.36.0",
|
||||
"oxlint-tsgolint": "^0.10.1",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.2.6",
|
||||
"vite-bundle-analyzer": "^1.2.3",
|
||||
"vite-tsconfig-paths": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue