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:
|
with:
|
||||||
image: local/zerobyte:ci
|
image: local/zerobyte:ci
|
||||||
fail-build: true
|
fail-build: true
|
||||||
|
only-fixed: true
|
||||||
severity-cutoff: critical
|
severity-cutoff: critical
|
||||||
|
|
||||||
- name: upload Anchore scan report
|
- 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 { 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 { inferAdditionalFields } from "better-auth/client/plugins";
|
||||||
import type { auth } from "~/lib/auth";
|
import type { auth } from "~/lib/auth";
|
||||||
|
|
||||||
export const authClient = createAuthClient({
|
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,
|
"when": 1767821088612,
|
||||||
"tag": "0030_lower-trim-username",
|
"tag": "0030_lower-trim-username",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 31,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1767863951955,
|
||||||
|
"tag": "0031_graceful_squadron_supreme",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -6,7 +6,7 @@ import {
|
||||||
type MiddlewareOptions,
|
type MiddlewareOptions,
|
||||||
} from "better-auth";
|
} from "better-auth";
|
||||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
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 { convertLegacyUserOnFirstLogin } from "./auth-middlewares/convert-legacy-user";
|
||||||
import { cryptoUtils } from "~/server/utils/crypto";
|
import { cryptoUtils } from "~/server/utils/crypto";
|
||||||
import { db } from "~/server/db/db";
|
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(),
|
emailVerified: integer("email_verified", { mode: "boolean" }).default(false).notNull(),
|
||||||
image: text("image"),
|
image: text("image"),
|
||||||
displayUsername: text("display_username"),
|
displayUsername: text("display_username"),
|
||||||
|
twoFactorEnabled: integer("two_factor_enabled", { mode: "boolean" }).notNull().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type User = typeof usersTable.$inferSelect;
|
export type User = typeof usersTable.$inferSelect;
|
||||||
export const sessionsTable = sqliteTable("sessions_table", {
|
export const sessionsTable = sqliteTable(
|
||||||
id: text().primaryKey(),
|
"sessions_table",
|
||||||
userId: text("user_id")
|
{
|
||||||
.notNull()
|
id: text().primaryKey(),
|
||||||
.references(() => usersTable.id, { onDelete: "cascade" }),
|
userId: text("user_id")
|
||||||
token: text("token").notNull().unique(),
|
.notNull()
|
||||||
expiresAt: int("expires_at", { mode: "timestamp_ms" }).notNull(),
|
.references(() => usersTable.id, { onDelete: "cascade" }),
|
||||||
createdAt: int("created_at", { mode: "timestamp_ms" })
|
token: text("token").notNull().unique(),
|
||||||
.notNull()
|
expiresAt: int("expires_at", { mode: "timestamp_ms" }).notNull(),
|
||||||
.default(sql`(unixepoch() * 1000)`),
|
createdAt: int("created_at", { mode: "timestamp_ms" })
|
||||||
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
.notNull()
|
||||||
.notNull()
|
.default(sql`(unixepoch() * 1000)`),
|
||||||
.$onUpdate(() => new Date())
|
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
||||||
.default(sql`(unixepoch() * 1000)`),
|
.notNull()
|
||||||
ipAddress: text("ip_address"),
|
.$onUpdate(() => new Date())
|
||||||
userAgent: text("user_agent"),
|
.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 type Session = typeof sessionsTable.$inferSelect;
|
||||||
|
|
||||||
export const account = sqliteTable(
|
export const account = sqliteTable(
|
||||||
|
|
@ -124,6 +129,7 @@ export const verification = sqliteTable(
|
||||||
export const userRelations = relations(usersTable, ({ many }) => ({
|
export const userRelations = relations(usersTable, ({ many }) => ({
|
||||||
sessions: many(sessionsTable),
|
sessions: many(sessionsTable),
|
||||||
accounts: many(account),
|
accounts: many(account),
|
||||||
|
twoFactors: many(twoFactor),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const sessionRelations = relations(sessionsTable, ({ one }) => ({
|
export const sessionRelations = relations(sessionsTable, ({ one }) => ({
|
||||||
|
|
@ -326,3 +332,16 @@ export const appMetadataTable = sqliteTable("app_metadata", {
|
||||||
.default(sql`(unixepoch() * 1000)`),
|
.default(sql`(unixepoch() * 1000)`),
|
||||||
});
|
});
|
||||||
export type AppMetadata = typeof appMetadataTable.$inferSelect;
|
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-openapi": "^1.1.1",
|
||||||
"hono-rate-limiter": "^0.5.3",
|
"hono-rate-limiter": "^0.5.3",
|
||||||
"http-errors-enhanced": "^4.0.2",
|
"http-errors-enhanced": "^4.0.2",
|
||||||
|
"input-otp": "^1.4.2",
|
||||||
"isbot": "^5.1.32",
|
"isbot": "^5.1.32",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^19.2.1",
|
"react": "^19.2.1",
|
||||||
"react-dom": "^19.2.1",
|
"react-dom": "^19.2.1",
|
||||||
"react-hook-form": "^7.70.0",
|
"react-hook-form": "^7.70.0",
|
||||||
|
|
@ -956,6 +958,8 @@
|
||||||
|
|
||||||
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],
|
||||||
|
|
|
||||||
229
package.json
229
package.json
|
|
@ -1,116 +1,117 @@
|
||||||
{
|
{
|
||||||
"name": "zerobyte",
|
"name": "zerobyte",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"packageManager": "bun@1.3.5",
|
||||||
"lint": "oxlint --type-aware",
|
"scripts": {
|
||||||
"build": "react-router build",
|
"lint": "oxlint --type-aware",
|
||||||
"dev": "bunx --bun vite",
|
"build": "react-router build",
|
||||||
"start": "bun ./dist/server/index.js",
|
"dev": "bunx --bun vite",
|
||||||
"cli:dev": "bun run app/server/cli/main.ts",
|
"start": "bun ./dist/server/index.js",
|
||||||
"cli": "ZEROBYTE_CLI=1 bun ./dist/server/index.js",
|
"cli:dev": "bun run app/server/cli/main.ts",
|
||||||
"tsc": "react-router typegen && tsc",
|
"cli": "ZEROBYTE_CLI=1 bun ./dist/server/index.js",
|
||||||
"start:dev": "docker compose down && docker compose up --build zerobyte-dev",
|
"tsc": "react-router typegen && tsc",
|
||||||
"start:prod": "docker compose down && docker compose up --build zerobyte-prod",
|
"start:dev": "docker compose down && docker compose up --build zerobyte-dev",
|
||||||
"gen:api-client": "openapi-ts",
|
"start:prod": "docker compose down && docker compose up --build zerobyte-prod",
|
||||||
"gen:migrations": "drizzle-kit generate",
|
"gen:api-client": "openapi-ts",
|
||||||
"studio": "drizzle-kit studio",
|
"gen:migrations": "drizzle-kit generate",
|
||||||
"test:server": "dotenv -e .env.test -- bun test app/server --preload ./app/test/setup.ts",
|
"studio": "drizzle-kit studio",
|
||||||
"test:client": "dotenv -e .env.test -- bun test app/client --preload ./app/test/setup-client.ts",
|
"test:server": "dotenv -e .env.test -- bun test app/server --preload ./app/test/setup.ts",
|
||||||
"test": "bun run test:server && bun run test:client"
|
"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",
|
"overrides": {
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"esbuild": "^0.27.2"
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
},
|
||||||
"@hono/standard-validator": "^0.2.0",
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@inquirer/prompts": "^8.0.2",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@hono/standard-validator": "^0.2.0",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@inquirer/prompts": "^8.0.2",
|
||||||
"@radix-ui/react-hover-card": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-progress": "^1.1.8",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
"@radix-ui/react-hover-card": "^1.1.15",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-progress": "^1.1.8",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@react-router/node": "^7.12.0",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@react-router/serve": "^7.12.0",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@scalar/hono-api-reference": "^0.9.32",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@tanstack/react-query": "^5.90.16",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"arktype": "^2.1.28",
|
"@react-router/node": "^7.10.0",
|
||||||
"better-auth": "^1.4.10",
|
"@react-router/serve": "^7.10.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"@scalar/hono-api-reference": "^0.9.25",
|
||||||
"clsx": "^2.1.1",
|
"@tanstack/react-query": "^5.90.11",
|
||||||
"commander": "^14.0.2",
|
"arktype": "^2.1.28",
|
||||||
"cron-parser": "^5.4.0",
|
"better-auth": "^1.4.10",
|
||||||
"date-fns": "^4.1.0",
|
"class-variance-authority": "^0.7.1",
|
||||||
"dither-plugin": "^1.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dotenv": "^17.2.3",
|
"commander": "^14.0.2",
|
||||||
"drizzle-orm": "^0.45.1",
|
"cron-parser": "^5.4.0",
|
||||||
"es-toolkit": "^1.42.0",
|
"date-fns": "^4.1.0",
|
||||||
"hono": "^4.11.3",
|
"dither-plugin": "^1.1.1",
|
||||||
"hono-openapi": "^1.1.1",
|
"dotenv": "^17.2.3",
|
||||||
"hono-rate-limiter": "^0.5.3",
|
"drizzle-orm": "^0.44.7",
|
||||||
"http-errors-enhanced": "^4.0.2",
|
"es-toolkit": "^1.42.0",
|
||||||
"isbot": "^5.1.32",
|
"hono": "4.10.5",
|
||||||
"lucide-react": "^0.562.0",
|
"hono-openapi": "^1.1.1",
|
||||||
"next-themes": "^0.4.6",
|
"hono-rate-limiter": "^0.5.0",
|
||||||
"node-cron": "^4.2.1",
|
"http-errors-enhanced": "^4.0.2",
|
||||||
"react": "^19.2.1",
|
"input-otp": "^1.4.2",
|
||||||
"react-dom": "^19.2.1",
|
"isbot": "^5.1.32",
|
||||||
"react-hook-form": "^7.70.0",
|
"lucide-react": "^0.555.0",
|
||||||
"react-markdown": "^10.1.0",
|
"next-themes": "^0.4.6",
|
||||||
"react-router": "^7.12.0",
|
"node-cron": "^4.2.1",
|
||||||
"react-router-hono-server": "^2.22.0",
|
"qrcode.react": "^4.2.0",
|
||||||
"recharts": "3.6.0",
|
"react": "^19.2.1",
|
||||||
"remark-gfm": "^4.0.1",
|
"react-dom": "^19.2.1",
|
||||||
"semver": "^7.7.3",
|
"react-hook-form": "^7.68.0",
|
||||||
"slugify": "^1.6.6",
|
"react-markdown": "^10.1.0",
|
||||||
"sonner": "^2.0.7",
|
"react-router": "^7.10.0",
|
||||||
"tailwind-merge": "^3.4.0",
|
"react-router-hono-server": "^2.22.0",
|
||||||
"tiny-typed-emitter": "^2.1.0",
|
"recharts": "3.5.1",
|
||||||
"winston": "^3.18.3",
|
"remark-gfm": "^4.0.1",
|
||||||
"yaml": "^2.8.2"
|
"semver": "^7.7.3",
|
||||||
},
|
"slugify": "^1.6.6",
|
||||||
"devDependencies": {
|
"sonner": "^2.0.7",
|
||||||
"@faker-js/faker": "^10.2.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"@happy-dom/global-registrator": "^20.1.0",
|
"tiny-typed-emitter": "^2.1.0",
|
||||||
"@hey-api/openapi-ts": "^0.90.2",
|
"winston": "^3.18.3",
|
||||||
"@react-router/dev": "^7.12.0",
|
"yaml": "^2.8.2"
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
},
|
||||||
"@tailwindcss/vite": "^4.1.17",
|
"devDependencies": {
|
||||||
"@tanstack/react-query-devtools": "^5.91.2",
|
"@faker-js/faker": "^10.1.0",
|
||||||
"@testing-library/dom": "^10.4.1",
|
"@happy-dom/global-registrator": "^20.0.11",
|
||||||
"@testing-library/react": "^16.3.1",
|
"@hey-api/openapi-ts": "^0.88.0",
|
||||||
"@types/bun": "^1.3.4",
|
"@react-router/dev": "^7.10.0",
|
||||||
"@types/node": "^25.0.3",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@types/react": "^19.2.7",
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@tanstack/react-query-devtools": "^5.91.1",
|
||||||
"@types/semver": "^7.7.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
"dotenv-cli": "^11.0.0",
|
"@testing-library/react": "^16.3.1",
|
||||||
"drizzle-kit": "^0.31.7",
|
"@types/bun": "^1.3.4",
|
||||||
"lightningcss": "^1.30.2",
|
"@types/node": "^25.0.3",
|
||||||
"oxfmt": "^0.23.0",
|
"@types/react": "^19.2.7",
|
||||||
"oxlint": "^1.38.0",
|
"@types/react-dom": "^19.2.3",
|
||||||
"oxlint-tsgolint": "^0.10.1",
|
"@types/semver": "^7.7.1",
|
||||||
"tailwindcss": "^4.1.17",
|
"dotenv-cli": "^11.0.0",
|
||||||
"tinyglobby": "^0.2.15",
|
"drizzle-kit": "^0.31.7",
|
||||||
"tw-animate-css": "^1.4.0",
|
"lightningcss": "^1.30.2",
|
||||||
"typescript": "^5.9.3",
|
"oxfmt": "^0.22.0",
|
||||||
"vite": "^7.3.1",
|
"oxlint": "^1.36.0",
|
||||||
"vite-bundle-analyzer": "^1.2.3",
|
"oxlint-tsgolint": "^0.10.1",
|
||||||
"vite-tsconfig-paths": "^6.0.3"
|
"tailwindcss": "^4.1.17",
|
||||||
},
|
"tinyglobby": "^0.2.15",
|
||||||
"overrides": {
|
"tw-animate-css": "^1.4.0",
|
||||||
"esbuild": "^0.27.2",
|
"typescript": "^5.9.3",
|
||||||
"hono": "^4.11.3"
|
"vite": "^7.2.6",
|
||||||
},
|
"vite-bundle-analyzer": "^1.2.3",
|
||||||
"packageManager": "bun@1.3.5"
|
"vite-tsconfig-paths": "^6.0.3"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue