mirror of
https://github.com/marcogll/TaxHacker_s23.git
synced 2026-01-13 13:25:18 +00:00
feat: config.js
This commit is contained in:
@@ -4,7 +4,7 @@ import DashboardUnsortedWidget from "@/components/dashboard/unsorted-widget"
|
|||||||
import { WelcomeWidget } from "@/components/dashboard/welcome-widget"
|
import { WelcomeWidget } from "@/components/dashboard/welcome-widget"
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
import { getCurrentUser } from "@/lib/auth"
|
import { getCurrentUser } from "@/lib/auth"
|
||||||
import { APP_DESCRIPTION } from "@/lib/constants"
|
import config from "@/lib/config"
|
||||||
import { getUnsortedFiles } from "@/models/files"
|
import { getUnsortedFiles } from "@/models/files"
|
||||||
import { getSettings } from "@/models/settings"
|
import { getSettings } from "@/models/settings"
|
||||||
import { TransactionFilters } from "@/models/transactions"
|
import { TransactionFilters } from "@/models/transactions"
|
||||||
@@ -12,7 +12,7 @@ import { Metadata } from "next"
|
|||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Dashboard",
|
title: "Dashboard",
|
||||||
description: APP_DESCRIPTION,
|
description: config.app.description,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function Dashboard({ searchParams }: { searchParams: Promise<TransactionFilters> }) {
|
export default async function Dashboard({ searchParams }: { searchParams: Promise<TransactionFilters> }) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { AppSidebar } from "@/components/sidebar/sidebar"
|
|||||||
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"
|
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"
|
||||||
import { Toaster } from "@/components/ui/sonner"
|
import { Toaster } from "@/components/ui/sonner"
|
||||||
import { getCurrentUser } from "@/lib/auth"
|
import { getCurrentUser } from "@/lib/auth"
|
||||||
import { APP_DESCRIPTION, APP_TITLE } from "@/lib/constants"
|
import config from "@/lib/config"
|
||||||
import { getUnsortedFilesCount } from "@/models/files"
|
import { getUnsortedFilesCount } from "@/models/files"
|
||||||
import type { Metadata, Viewport } from "next"
|
import type { Metadata, Viewport } from "next"
|
||||||
import "../globals.css"
|
import "../globals.css"
|
||||||
@@ -13,9 +13,9 @@ import { NotificationProvider } from "./context"
|
|||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: {
|
title: {
|
||||||
template: "%s | TaxHacker",
|
template: "%s | TaxHacker",
|
||||||
default: APP_TITLE,
|
default: config.app.title,
|
||||||
},
|
},
|
||||||
description: APP_DESCRIPTION,
|
description: config.app.description,
|
||||||
icons: {
|
icons: {
|
||||||
icon: "/favicon.ico",
|
icon: "/favicon.ico",
|
||||||
shortcut: "/favicon.ico",
|
shortcut: "/favicon.ico",
|
||||||
@@ -38,13 +38,14 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
|||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<MobileMenu unsortedFilesCount={unsortedFilesCount} />
|
<MobileMenu unsortedFilesCount={unsortedFilesCount} />
|
||||||
<AppSidebar
|
<AppSidebar
|
||||||
unsortedFilesCount={unsortedFilesCount}
|
|
||||||
profile={{
|
profile={{
|
||||||
id: user.id,
|
id: user.id,
|
||||||
name: user.name || "",
|
name: user.name || "",
|
||||||
email: user.email,
|
email: user.email,
|
||||||
avatar: user.avatar || undefined,
|
avatar: user.avatar || undefined,
|
||||||
}}
|
}}
|
||||||
|
unsortedFilesCount={unsortedFilesCount}
|
||||||
|
isSelfHosted={config.selfHosted.isEnabled}
|
||||||
/>
|
/>
|
||||||
<SidebarInset className="w-full h-full mt-[60px] md:mt-0 overflow-auto">{children}</SidebarInset>
|
<SidebarInset className="w-full h-full mt-[60px] md:mt-0 overflow-auto">{children}</SidebarInset>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import LLMSettingsForm from "@/components/settings/llm-settings-form"
|
import LLMSettingsForm from "@/components/settings/llm-settings-form"
|
||||||
import { getCurrentUser } from "@/lib/auth"
|
import { getCurrentUser } from "@/lib/auth"
|
||||||
|
import config from "@/lib/config"
|
||||||
import { getFields } from "@/models/fields"
|
import { getFields } from "@/models/fields"
|
||||||
import { getSettings } from "@/models/settings"
|
import { getSettings } from "@/models/settings"
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ export default async function LlmSettingsPage() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="w-full max-w-2xl">
|
<div className="w-full max-w-2xl">
|
||||||
<LLMSettingsForm settings={settings} fields={fields} />
|
<LLMSettingsForm settings={settings} fields={fields} showApiKey={config.selfHosted.isEnabled} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { buildLLMPrompt } from "@/ai/prompt"
|
|||||||
import { fieldsToJsonSchema } from "@/ai/schema"
|
import { fieldsToJsonSchema } from "@/ai/schema"
|
||||||
import { transactionFormSchema } from "@/forms/transactions"
|
import { transactionFormSchema } from "@/forms/transactions"
|
||||||
import { getCurrentUser } from "@/lib/auth"
|
import { getCurrentUser } from "@/lib/auth"
|
||||||
import { IS_SELF_HOSTED_MODE } from "@/lib/constants"
|
import config from "@/lib/config"
|
||||||
import { getTransactionFileUploadPath, getUserUploadsDirectory } from "@/lib/files"
|
import { getTransactionFileUploadPath, getUserUploadsDirectory } from "@/lib/files"
|
||||||
import { DEFAULT_PROMPT_ANALYSE_NEW_FILE } from "@/models/defaults"
|
import { DEFAULT_PROMPT_ANALYSE_NEW_FILE } from "@/models/defaults"
|
||||||
import { deleteFile, getFileById, updateFile } from "@/models/files"
|
import { deleteFile, getFileById, updateFile } from "@/models/files"
|
||||||
@@ -50,7 +50,7 @@ export async function analyzeFileAction(
|
|||||||
prompt,
|
prompt,
|
||||||
schema,
|
schema,
|
||||||
attachments,
|
attachments,
|
||||||
IS_SELF_HOSTED_MODE ? settings.openai_api_key : process.env.OPENAI_API_KEY || ""
|
config.selfHosted.isEnabled ? settings.openai_api_key : process.env.OPENAI_API_KEY || ""
|
||||||
)
|
)
|
||||||
|
|
||||||
console.log("Analysis results:", results)
|
console.log("Analysis results:", results)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button"
|
|||||||
import { Card } from "@/components/ui/card"
|
import { Card } from "@/components/ui/card"
|
||||||
import AnalyzeForm from "@/components/unsorted/analyze-form"
|
import AnalyzeForm from "@/components/unsorted/analyze-form"
|
||||||
import { getCurrentUser } from "@/lib/auth"
|
import { getCurrentUser } from "@/lib/auth"
|
||||||
import { IS_SELF_HOSTED_MODE } from "@/lib/constants"
|
import config from "@/lib/config"
|
||||||
import { getCategories } from "@/models/categories"
|
import { getCategories } from "@/models/categories"
|
||||||
import { getCurrencies } from "@/models/currencies"
|
import { getCurrencies } from "@/models/currencies"
|
||||||
import { getFields } from "@/models/fields"
|
import { getFields } from "@/models/fields"
|
||||||
@@ -36,7 +36,7 @@ export default async function UnsortedPage() {
|
|||||||
<h2 className="text-3xl font-bold tracking-tight">You have {files.length} unsorted files</h2>
|
<h2 className="text-3xl font-bold tracking-tight">You have {files.length} unsorted files</h2>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{IS_SELF_HOSTED_MODE && !settings.openai_api_key && (
|
{config.selfHosted.isEnabled && !settings.openai_api_key && (
|
||||||
<Alert>
|
<Alert>
|
||||||
<Settings className="h-4 w-4 mt-2" />
|
<Settings className="h-4 w-4 mt-2" />
|
||||||
<div className="flex flex-row justify-between pt-2">
|
<div className="flex flex-row justify-between pt-2">
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { LoginForm } from "@/components/auth/login-form"
|
import { LoginForm } from "@/components/auth/login-form"
|
||||||
import { Card, CardContent, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardTitle } from "@/components/ui/card"
|
||||||
import { ColoredText } from "@/components/ui/colored-text"
|
import { ColoredText } from "@/components/ui/colored-text"
|
||||||
import { IS_SELF_HOSTED_MODE, SELF_HOSTED_REDIRECT_URL } from "@/lib/constants"
|
import config from "@/lib/config"
|
||||||
import { redirect } from "next/navigation"
|
import { redirect } from "next/navigation"
|
||||||
|
|
||||||
export default async function LoginPage() {
|
export default async function LoginPage() {
|
||||||
if (IS_SELF_HOSTED_MODE) {
|
if (config.selfHosted.isEnabled) {
|
||||||
redirect(SELF_HOSTED_REDIRECT_URL)
|
redirect(config.selfHosted.redirectUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { FormInput } from "@/components/forms/simple"
|
|||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Card, CardDescription, CardTitle } from "@/components/ui/card"
|
import { Card, CardDescription, CardTitle } from "@/components/ui/card"
|
||||||
import { ColoredText } from "@/components/ui/colored-text"
|
import { ColoredText } from "@/components/ui/colored-text"
|
||||||
import { IS_SELF_HOSTED_MODE, SELF_HOSTED_REDIRECT_URL } from "@/lib/constants"
|
import config from "@/lib/config"
|
||||||
import { DEFAULT_CURRENCIES, DEFAULT_SETTINGS } from "@/models/defaults"
|
import { DEFAULT_CURRENCIES, DEFAULT_SETTINGS } from "@/models/defaults"
|
||||||
import { getSelfHostedUser } from "@/models/users"
|
import { getSelfHostedUser } from "@/models/users"
|
||||||
import { ShieldAlert } from "lucide-react"
|
import { ShieldAlert } from "lucide-react"
|
||||||
@@ -11,7 +11,7 @@ import { redirect } from "next/navigation"
|
|||||||
import { selfHostedGetStartedAction } from "../actions"
|
import { selfHostedGetStartedAction } from "../actions"
|
||||||
|
|
||||||
export default async function SelfHostedWelcomePage() {
|
export default async function SelfHostedWelcomePage() {
|
||||||
if (!IS_SELF_HOSTED_MODE) {
|
if (!config.selfHosted.isEnabled) {
|
||||||
return (
|
return (
|
||||||
<Card className="w-full max-w-xl mx-auto p-8 flex flex-col items-center justify-center gap-6">
|
<Card className="w-full max-w-xl mx-auto p-8 flex flex-col items-center justify-center gap-6">
|
||||||
<CardTitle className="text-2xl font-bold flex items-center gap-2">
|
<CardTitle className="text-2xl font-bold flex items-center gap-2">
|
||||||
@@ -31,7 +31,7 @@ export default async function SelfHostedWelcomePage() {
|
|||||||
|
|
||||||
const user = await getSelfHostedUser()
|
const user = await getSelfHostedUser()
|
||||||
if (user) {
|
if (user) {
|
||||||
redirect(SELF_HOSTED_REDIRECT_URL)
|
redirect(config.selfHosted.redirectUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { AUTH_LOGIN_URL, IS_SELF_HOSTED_MODE, SELF_HOSTED_WELCOME_URL } from "@/lib/constants"
|
import config from "@/lib/config"
|
||||||
import { createUserDefaults, isDatabaseEmpty } from "@/models/defaults"
|
import { createUserDefaults, isDatabaseEmpty } from "@/models/defaults"
|
||||||
import { getSelfHostedUser } from "@/models/users"
|
import { getSelfHostedUser } from "@/models/users"
|
||||||
import { revalidatePath } from "next/cache"
|
import { revalidatePath } from "next/cache"
|
||||||
import { redirect } from "next/navigation"
|
import { redirect } from "next/navigation"
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
if (!IS_SELF_HOSTED_MODE) {
|
if (!config.selfHosted.isEnabled) {
|
||||||
redirect(AUTH_LOGIN_URL)
|
redirect(config.auth.loginUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await getSelfHostedUser()
|
const user = await getSelfHostedUser()
|
||||||
if (!user) {
|
if (!user) {
|
||||||
redirect(SELF_HOSTED_WELCOME_URL)
|
redirect(config.selfHosted.welcomeUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await isDatabaseEmpty(user.id)) {
|
if (await isDatabaseEmpty(user.id)) {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
|
import SignupForm from "@/components/auth/signup-form"
|
||||||
import { Card, CardContent, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardTitle } from "@/components/ui/card"
|
||||||
import { ColoredText } from "@/components/ui/colored-text"
|
import { ColoredText } from "@/components/ui/colored-text"
|
||||||
import { IS_SELF_HOSTED_MODE, SELF_HOSTED_REDIRECT_URL } from "@/lib/constants"
|
import config from "@/lib/config"
|
||||||
import { redirect } from "next/navigation"
|
import { redirect } from "next/navigation"
|
||||||
|
|
||||||
export default async function LoginPage() {
|
export default async function LoginPage() {
|
||||||
if (IS_SELF_HOSTED_MODE) {
|
if (config.selfHosted.isEnabled) {
|
||||||
redirect(SELF_HOSTED_REDIRECT_URL)
|
redirect(config.selfHosted.redirectUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -15,10 +16,13 @@ export default async function LoginPage() {
|
|||||||
<ColoredText>TaxHacker: Cloud Edition</ColoredText>
|
<ColoredText>TaxHacker: Cloud Edition</ColoredText>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardContent className="w-full">
|
<CardContent className="w-full">
|
||||||
<div className="text-center text-md text-muted-foreground">
|
{config.auth.disableSignup ? (
|
||||||
Creating new account is disabled for now. Please use the self-hosted version.
|
<div className="text-center text-md text-muted-foreground">
|
||||||
</div>
|
Creating new account is disabled for now. Please use the self-hosted version.
|
||||||
{/* <SignupForm /> */}
|
</div>
|
||||||
|
) : (
|
||||||
|
<SignupForm />
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use server"
|
"use server"
|
||||||
|
|
||||||
|
import config from "@/lib/config"
|
||||||
import { resend, sendNewsletterWelcomeEmail } from "@/lib/email"
|
import { resend, sendNewsletterWelcomeEmail } from "@/lib/email"
|
||||||
|
|
||||||
export async function subscribeToNewsletterAction(email: string) {
|
export async function subscribeToNewsletterAction(email: string) {
|
||||||
@@ -9,7 +10,7 @@ export async function subscribeToNewsletterAction(email: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const existingContacts = await resend.contacts.list({
|
const existingContacts = await resend.contacts.list({
|
||||||
audienceId: process.env.RESEND_AUDIENCE_ID as string,
|
audienceId: config.email.audienceId,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (existingContacts.data) {
|
if (existingContacts.data) {
|
||||||
@@ -22,7 +23,7 @@ export async function subscribeToNewsletterAction(email: string) {
|
|||||||
|
|
||||||
await resend.contacts.create({
|
await resend.contacts.create({
|
||||||
email,
|
email,
|
||||||
audienceId: process.env.RESEND_AUDIENCE_ID as string,
|
audienceId: config.email.audienceId,
|
||||||
unsubscribed: false,
|
unsubscribed: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
|
import config from "@/lib/config"
|
||||||
import type { Metadata, Viewport } from "next"
|
import type { Metadata, Viewport } from "next"
|
||||||
import "./globals.css"
|
import "./globals.css"
|
||||||
import { APP_DESCRIPTION, APP_TITLE } from "@/lib/constants"
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: {
|
title: {
|
||||||
template: "%s | TaxHacker",
|
template: "%s | TaxHacker",
|
||||||
default: APP_TITLE,
|
default: config.app.title,
|
||||||
},
|
},
|
||||||
description: APP_DESCRIPTION,
|
description: config.app.description,
|
||||||
icons: {
|
icons: {
|
||||||
icon: "/favicon.ico",
|
icon: "/favicon.ico",
|
||||||
shortcut: "/favicon.ico",
|
shortcut: "/favicon.ico",
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import LandingPage from "@/app/landing/landing"
|
import LandingPage from "@/app/landing/landing"
|
||||||
import { getSession } from "@/lib/auth"
|
import { getSession } from "@/lib/auth"
|
||||||
import { IS_SELF_HOSTED_MODE, SELF_HOSTED_REDIRECT_URL } from "@/lib/constants"
|
import config from "@/lib/config"
|
||||||
import { redirect } from "next/navigation"
|
import { redirect } from "next/navigation"
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
const session = await getSession()
|
const session = await getSession()
|
||||||
if (!session) {
|
if (!session) {
|
||||||
if (IS_SELF_HOSTED_MODE) {
|
if (config.selfHosted.isEnabled) {
|
||||||
redirect(SELF_HOSTED_REDIRECT_URL)
|
redirect(config.selfHosted.redirectUrl)
|
||||||
}
|
}
|
||||||
return <LandingPage />
|
return <LandingPage />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useNotification } from "@/app/(app)/context"
|
import { useNotification } from "@/app/(app)/context"
|
||||||
import { uploadFilesAction } from "@/app/(app)/files/actions"
|
import { uploadFilesAction } from "@/app/(app)/files/actions"
|
||||||
import { FormError } from "@/components/forms/error"
|
import { FormError } from "@/components/forms/error"
|
||||||
import { FILE_ACCEPTED_MIMETYPES } from "@/lib/constants"
|
import config from "@/lib/config"
|
||||||
import { Camera, Loader2 } from "lucide-react"
|
import { Camera, Loader2 } from "lucide-react"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { startTransition, useState } from "react"
|
import { startTransition, useState } from "react"
|
||||||
@@ -48,7 +48,7 @@ export default function DashboardDropZoneWidget() {
|
|||||||
id="fileInput"
|
id="fileInput"
|
||||||
className="hidden"
|
className="hidden"
|
||||||
multiple
|
multiple
|
||||||
accept={FILE_ACCEPTED_MIMETYPES}
|
accept={config.upload.acceptedMimeTypes}
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col items-center justify-center gap-4 p-8 text-center h-full">
|
<div className="flex flex-col items-center justify-center gap-4 p-8 text-center h-full">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useNotification } from "@/app/(app)/context"
|
import { useNotification } from "@/app/(app)/context"
|
||||||
import { uploadFilesAction } from "@/app/(app)/files/actions"
|
import { uploadFilesAction } from "@/app/(app)/files/actions"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { FILE_ACCEPTED_MIMETYPES } from "@/lib/constants"
|
import config from "@/lib/config"
|
||||||
import { Loader2 } from "lucide-react"
|
import { Loader2 } from "lucide-react"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { ComponentProps, startTransition, useRef, useState } from "react"
|
import { ComponentProps, startTransition, useRef, useState } from "react"
|
||||||
@@ -54,7 +54,7 @@ export function UploadButton({ children, ...props }: { children: React.ReactNode
|
|||||||
id="fileInput"
|
id="fileInput"
|
||||||
className="hidden"
|
className="hidden"
|
||||||
multiple
|
multiple
|
||||||
accept={FILE_ACCEPTED_MIMETYPES}
|
accept={config.upload.acceptedMimeTypes}
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -6,29 +6,40 @@ import { FormError } from "@/components/forms/error"
|
|||||||
import { FormInput, FormTextarea } from "@/components/forms/simple"
|
import { FormInput, FormTextarea } from "@/components/forms/simple"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Card, CardTitle } from "@/components/ui/card"
|
import { Card, CardTitle } from "@/components/ui/card"
|
||||||
import { IS_SELF_HOSTED_MODE } from "@/lib/constants"
|
|
||||||
import { Field } from "@prisma/client"
|
import { Field } from "@prisma/client"
|
||||||
import { CircleCheckBig, Edit } from "lucide-react"
|
import { CircleCheckBig, Edit } from "lucide-react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { useActionState } from "react"
|
import { useActionState } from "react"
|
||||||
|
|
||||||
export default function LLMSettingsForm({ settings, fields }: { settings: Record<string, string>; fields: Field[] }) {
|
export default function LLMSettingsForm({
|
||||||
|
settings,
|
||||||
|
fields,
|
||||||
|
showApiKey = true,
|
||||||
|
}: {
|
||||||
|
settings: Record<string, string>
|
||||||
|
fields: Field[]
|
||||||
|
showApiKey?: boolean
|
||||||
|
}) {
|
||||||
const [saveState, saveAction, pending] = useActionState(saveSettingsAction, null)
|
const [saveState, saveAction, pending] = useActionState(saveSettingsAction, null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form action={saveAction} className="space-y-4">
|
<form action={saveAction} className="space-y-4">
|
||||||
{IS_SELF_HOSTED_MODE && (
|
{showApiKey && (
|
||||||
<FormInput title="OpenAI API Key" name="openai_api_key" defaultValue={settings.openai_api_key} />
|
<>
|
||||||
)}
|
<FormInput title="OpenAI API Key" name="openai_api_key" defaultValue={settings.openai_api_key} />
|
||||||
|
|
||||||
{IS_SELF_HOSTED_MODE && (
|
<small className="text-muted-foreground">
|
||||||
<small className="text-muted-foreground">
|
Get your API key from{" "}
|
||||||
Get your API key from{" "}
|
<a
|
||||||
<a href="https://platform.openai.com/settings/organization/api-keys" target="_blank" className="underline">
|
href="https://platform.openai.com/settings/organization/api-keys"
|
||||||
OpenAI Platform Console
|
target="_blank"
|
||||||
</a>
|
className="underline"
|
||||||
</small>
|
>
|
||||||
|
OpenAI Platform Console
|
||||||
|
</a>
|
||||||
|
</small>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<FormTextarea
|
<FormTextarea
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||||
import { useSidebar } from "@/components/ui/sidebar"
|
import { useSidebar } from "@/components/ui/sidebar"
|
||||||
import { APP_TITLE } from "@/lib/constants"
|
import config from "@/lib/config"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
|
|
||||||
export default function MobileMenu({ unsortedFilesCount }: { unsortedFilesCount: number }) {
|
export default function MobileMenu({ unsortedFilesCount }: { unsortedFilesCount: number }) {
|
||||||
@@ -15,7 +15,7 @@ export default function MobileMenu({ unsortedFilesCount }: { unsortedFilesCount:
|
|||||||
<AvatarFallback className="rounded-lg">AI</AvatarFallback>
|
<AvatarFallback className="rounded-lg">AI</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Link href="/" className="text-lg font-bold">
|
<Link href="/" className="text-lg font-bold">
|
||||||
{APP_TITLE}
|
{config.app.title}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/unsorted"
|
href="/unsorted"
|
||||||
|
|||||||
@@ -10,12 +10,11 @@ import {
|
|||||||
import { SidebarMenuButton } from "@/components/ui/sidebar"
|
import { SidebarMenuButton } from "@/components/ui/sidebar"
|
||||||
import { UserProfile } from "@/lib/auth"
|
import { UserProfile } from "@/lib/auth"
|
||||||
import { authClient } from "@/lib/auth-client"
|
import { authClient } from "@/lib/auth-client"
|
||||||
import { IS_SELF_HOSTED_MODE } from "@/lib/constants"
|
|
||||||
import { LogOut, MoreVertical, User } from "lucide-react"
|
import { LogOut, MoreVertical, User } from "lucide-react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { redirect } from "next/navigation"
|
import { redirect } from "next/navigation"
|
||||||
|
|
||||||
export default function SidebarUser({ profile }: { profile: UserProfile }) {
|
export default function SidebarUser({ profile, isSelfHosted }: { profile: UserProfile; isSelfHosted: boolean }) {
|
||||||
const signOut = async () => {
|
const signOut = async () => {
|
||||||
await authClient.signOut({})
|
await authClient.signOut({})
|
||||||
redirect("/")
|
redirect("/")
|
||||||
@@ -61,14 +60,16 @@ export default function SidebarUser({ profile }: { profile: UserProfile }) {
|
|||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem> */}
|
</DropdownMenuItem> */}
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator />
|
{!isSelfHosted && (
|
||||||
{!IS_SELF_HOSTED_MODE && (
|
<>
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuSeparator />
|
||||||
<span onClick={signOut} className="flex items-center gap-2 text-red-600 cursor-pointer">
|
<DropdownMenuItem asChild>
|
||||||
<LogOut className="h-4 w-4" />
|
<span onClick={signOut} className="flex items-center gap-2 text-red-600 cursor-pointer">
|
||||||
Log out
|
<LogOut className="h-4 w-4" />
|
||||||
</span>
|
Log out
|
||||||
</DropdownMenuItem>
|
</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
useSidebar,
|
useSidebar,
|
||||||
} from "@/components/ui/sidebar"
|
} from "@/components/ui/sidebar"
|
||||||
import { UserProfile } from "@/lib/auth"
|
import { UserProfile } from "@/lib/auth"
|
||||||
import { APP_TITLE, IS_SELF_HOSTED_MODE } from "@/lib/constants"
|
import config from "@/lib/config"
|
||||||
import { ClockArrowUp, FileText, Import, LayoutDashboard, Settings, Sparkles, Upload } from "lucide-react"
|
import { ClockArrowUp, FileText, Import, LayoutDashboard, Settings, Sparkles, Upload } from "lucide-react"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
@@ -28,7 +28,15 @@ import { Blinker } from "./blinker"
|
|||||||
import { SidebarMenuItemWithHighlight } from "./sidebar-item"
|
import { SidebarMenuItemWithHighlight } from "./sidebar-item"
|
||||||
import SidebarUser from "./sidebar-user"
|
import SidebarUser from "./sidebar-user"
|
||||||
|
|
||||||
export function AppSidebar({ unsortedFilesCount, profile }: { unsortedFilesCount: number; profile: UserProfile }) {
|
export function AppSidebar({
|
||||||
|
profile,
|
||||||
|
unsortedFilesCount,
|
||||||
|
isSelfHosted,
|
||||||
|
}: {
|
||||||
|
profile: UserProfile
|
||||||
|
unsortedFilesCount: number
|
||||||
|
isSelfHosted: boolean
|
||||||
|
}) {
|
||||||
const { open, setOpenMobile } = useSidebar()
|
const { open, setOpenMobile } = useSidebar()
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const { notification } = useNotification()
|
const { notification } = useNotification()
|
||||||
@@ -46,7 +54,7 @@ export function AppSidebar({ unsortedFilesCount, profile }: { unsortedFilesCount
|
|||||||
<Image src="/logo/256.png" alt="Logo" className="h-10 w-10 rounded-lg" width={40} height={40} />
|
<Image src="/logo/256.png" alt="Logo" className="h-10 w-10 rounded-lg" width={40} height={40} />
|
||||||
<div className="grid flex-1 text-left leading-tight">
|
<div className="grid flex-1 text-left leading-tight">
|
||||||
<span className="truncate font-semibold text-lg">
|
<span className="truncate font-semibold text-lg">
|
||||||
<ColoredText>{APP_TITLE}</ColoredText>
|
<ColoredText>{config.app.title}</ColoredText>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -124,7 +132,7 @@ export function AppSidebar({ unsortedFilesCount, profile }: { unsortedFilesCount
|
|||||||
</Link>
|
</Link>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
{IS_SELF_HOSTED_MODE && (
|
{isSelfHosted && (
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton asChild>
|
<SidebarMenuButton asChild>
|
||||||
<Link href="https://vas3k.com/donate/" target="_blank">
|
<Link href="https://vas3k.com/donate/" target="_blank">
|
||||||
@@ -146,7 +154,7 @@ export function AppSidebar({ unsortedFilesCount, profile }: { unsortedFilesCount
|
|||||||
<SidebarGroupContent>
|
<SidebarGroupContent>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarUser profile={profile} />
|
<SidebarUser profile={profile} isSelfHosted={isSelfHosted} />
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroupContent>
|
</SidebarGroupContent>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { deleteTransactionFileAction, uploadTransactionFilesAction } from "@/app
|
|||||||
import { FilePreview } from "@/components/files/preview"
|
import { FilePreview } from "@/components/files/preview"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Card } from "@/components/ui/card"
|
import { Card } from "@/components/ui/card"
|
||||||
import { FILE_ACCEPTED_MIMETYPES } from "@/lib/constants"
|
import config from "@/lib/config"
|
||||||
import { File, Transaction } from "@prisma/client"
|
import { File, Transaction } from "@prisma/client"
|
||||||
import { Loader2, Upload, X } from "lucide-react"
|
import { Loader2, Upload, X } from "lucide-react"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
@@ -72,7 +72,7 @@ export default function TransactionFiles({ transaction, files }: { transaction:
|
|||||||
name="file"
|
name="file"
|
||||||
className="absolute inset-0 top-0 left-0 w-full h-full opacity-0"
|
className="absolute inset-0 top-0 left-0 w-full h-full opacity-0"
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
accept={FILE_ACCEPTED_MIMETYPES}
|
accept={config.upload.acceptedMimeTypes}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
20
lib/auth.ts
20
lib/auth.ts
@@ -1,6 +1,6 @@
|
|||||||
import { AUTH_LOGIN_URL, IS_SELF_HOSTED_MODE, SELF_HOSTED_REDIRECT_URL } from "@/lib/constants"
|
import config from "@/lib/config"
|
||||||
import { createUserDefaults } from "@/models/defaults"
|
import { createUserDefaults } from "@/models/defaults"
|
||||||
import { getSelfHostedUser, getUserByEmail } from "@/models/users"
|
import { getSelfHostedUser } from "@/models/users"
|
||||||
import { User } from "@prisma/client"
|
import { User } from "@prisma/client"
|
||||||
import { betterAuth } from "better-auth"
|
import { betterAuth } from "better-auth"
|
||||||
import { prismaAdapter } from "better-auth/adapters/prisma"
|
import { prismaAdapter } from "better-auth/adapters/prisma"
|
||||||
@@ -22,7 +22,7 @@ export const auth = betterAuth({
|
|||||||
database: prismaAdapter(prisma, { provider: "postgresql" }),
|
database: prismaAdapter(prisma, { provider: "postgresql" }),
|
||||||
email: {
|
email: {
|
||||||
provider: "resend",
|
provider: "resend",
|
||||||
from: process.env.RESEND_FROM_EMAIL!,
|
from: config.email.from,
|
||||||
resend,
|
resend,
|
||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
@@ -49,14 +49,10 @@ export const auth = betterAuth({
|
|||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
emailOTP({
|
emailOTP({
|
||||||
disableSignUp: true,
|
disableSignUp: config.auth.disableSignup,
|
||||||
otpLength: 6,
|
otpLength: 6,
|
||||||
expiresIn: 10 * 60, // 10 minutes
|
expiresIn: 10 * 60, // 10 minutes
|
||||||
sendVerificationOTP: async ({ email, otp }) => {
|
sendVerificationOTP: async ({ email, otp }) => {
|
||||||
const user = await getUserByEmail(email as string)
|
|
||||||
if (!user) {
|
|
||||||
throw new Error("User with this email does not exist")
|
|
||||||
}
|
|
||||||
await sendOTPCodeEmail({ email, otp })
|
await sendOTPCodeEmail({ email, otp })
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -65,7 +61,7 @@ export const auth = betterAuth({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export async function getSession() {
|
export async function getSession() {
|
||||||
if (IS_SELF_HOSTED_MODE) {
|
if (config.selfHosted.isEnabled) {
|
||||||
const user = await getSelfHostedUser()
|
const user = await getSelfHostedUser()
|
||||||
return user ? { user } : null
|
return user ? { user } : null
|
||||||
}
|
}
|
||||||
@@ -78,10 +74,10 @@ export async function getSession() {
|
|||||||
export async function getCurrentUser(): Promise<User> {
|
export async function getCurrentUser(): Promise<User> {
|
||||||
const session = await getSession()
|
const session = await getSession()
|
||||||
if (!session || !session.user) {
|
if (!session || !session.user) {
|
||||||
if (IS_SELF_HOSTED_MODE) {
|
if (config.selfHosted.isEnabled) {
|
||||||
redirect(SELF_HOSTED_REDIRECT_URL)
|
redirect(config.selfHosted.redirectUrl)
|
||||||
} else {
|
} else {
|
||||||
redirect(AUTH_LOGIN_URL)
|
redirect(config.auth.loginUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return session.user as User
|
return session.user as User
|
||||||
|
|||||||
26
lib/config.ts
Normal file
26
lib/config.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
const config = {
|
||||||
|
app: {
|
||||||
|
title: "TaxHacker",
|
||||||
|
description: "Your personal AI accountant",
|
||||||
|
version: process.env.npm_package_version || "0.0.1",
|
||||||
|
},
|
||||||
|
upload: {
|
||||||
|
acceptedMimeTypes: "image/*,.pdf,.doc,.docx,.xls,.xlsx",
|
||||||
|
},
|
||||||
|
selfHosted: {
|
||||||
|
isEnabled: process.env.SELF_HOSTED_MODE === "true",
|
||||||
|
redirectUrl: "/self-hosted/redirect",
|
||||||
|
welcomeUrl: "/self-hosted",
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
loginUrl: "/enter",
|
||||||
|
disableSignup: process.env.DISABLE_SIGNUP === "true" || process.env.SELF_HOSTED_MODE === "true",
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
apiKey: process.env.RESEND_API_KEY || "",
|
||||||
|
from: process.env.RESEND_FROM_EMAIL || "",
|
||||||
|
audienceId: process.env.RESEND_AUDIENCE_ID || "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export const APP_TITLE = "TaxHacker"
|
|
||||||
export const APP_DESCRIPTION = "Your personal AI accountant"
|
|
||||||
export const FILE_ACCEPTED_MIMETYPES = "image/*,.pdf,.doc,.docx,.xls,.xlsx"
|
|
||||||
export const IS_SELF_HOSTED_MODE = process.env.SELF_HOSTED_MODE === "true"
|
|
||||||
export const SELF_HOSTED_REDIRECT_URL = "/self-hosted/redirect"
|
|
||||||
export const SELF_HOSTED_WELCOME_URL = "/self-hosted"
|
|
||||||
export const AUTH_LOGIN_URL = "/enter"
|
|
||||||
@@ -27,9 +27,6 @@ export async function getCurrencyRate(currencyCodeFrom: string, currencyCodeTo:
|
|||||||
export async function fetchHistoricalCurrencyRates(currency: string = "USD", date: Date): Promise<HistoricRate[]> {
|
export async function fetchHistoricalCurrencyRates(currency: string = "USD", date: Date): Promise<HistoricRate[]> {
|
||||||
const formattedDate = format(date, "yyyy-MM-dd")
|
const formattedDate = format(date, "yyyy-MM-dd")
|
||||||
|
|
||||||
console.log("DATE", formattedDate)
|
|
||||||
console.log("QUERY", encodeURIComponent(`https://www.xe.com/currencytables/?from=${currency}&date=${formattedDate}`))
|
|
||||||
|
|
||||||
const url = `https://corsproxy.io/?url=${encodeURIComponent(
|
const url = `https://corsproxy.io/?url=${encodeURIComponent(
|
||||||
`https://www.xe.com/currencytables/?from=${currency}&date=${formattedDate}`
|
`https://www.xe.com/currencytables/?from=${currency}&date=${formattedDate}`
|
||||||
)}`
|
)}`
|
||||||
|
|||||||
@@ -2,14 +2,15 @@ import { NewsletterWelcomeEmail } from "@/components/emails/newsletter-welcome-e
|
|||||||
import { OTPEmail } from "@/components/emails/otp-email"
|
import { OTPEmail } from "@/components/emails/otp-email"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { Resend } from "resend"
|
import { Resend } from "resend"
|
||||||
|
import config from "./config"
|
||||||
|
|
||||||
export const resend = new Resend(process.env.RESEND_API_KEY)
|
export const resend = new Resend(config.email.apiKey)
|
||||||
|
|
||||||
export async function sendOTPCodeEmail({ email, otp }: { email: string; otp: string }) {
|
export async function sendOTPCodeEmail({ email, otp }: { email: string; otp: string }) {
|
||||||
const html = React.createElement(OTPEmail, { otp })
|
const html = React.createElement(OTPEmail, { otp })
|
||||||
|
|
||||||
return await resend.emails.send({
|
return await resend.emails.send({
|
||||||
from: process.env.RESEND_FROM_EMAIL!,
|
from: config.email.from,
|
||||||
to: email,
|
to: email,
|
||||||
subject: "Your TaxHacker verification code",
|
subject: "Your TaxHacker verification code",
|
||||||
react: html,
|
react: html,
|
||||||
@@ -20,7 +21,7 @@ export async function sendNewsletterWelcomeEmail(email: string) {
|
|||||||
const html = React.createElement(NewsletterWelcomeEmail)
|
const html = React.createElement(NewsletterWelcomeEmail)
|
||||||
|
|
||||||
return await resend.emails.send({
|
return await resend.emails.send({
|
||||||
from: process.env.RESEND_FROM_EMAIL as string,
|
from: config.email.from,
|
||||||
to: email,
|
to: email,
|
||||||
subject: "Welcome to TaxHacker Newsletter!",
|
subject: "Welcome to TaxHacker Newsletter!",
|
||||||
react: html,
|
react: html,
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
|
import { default as globalConfig } from "@/lib/config"
|
||||||
import { getSessionCookie } from "better-auth/cookies"
|
import { getSessionCookie } from "better-auth/cookies"
|
||||||
import { NextRequest, NextResponse } from "next/server"
|
import { NextRequest, NextResponse } from "next/server"
|
||||||
import { AUTH_LOGIN_URL, IS_SELF_HOSTED_MODE } from "./lib/constants"
|
|
||||||
|
|
||||||
export default async function middleware(request: NextRequest) {
|
export default async function middleware(request: NextRequest) {
|
||||||
if (IS_SELF_HOSTED_MODE) {
|
if (globalConfig.selfHosted.isEnabled) {
|
||||||
return NextResponse.next()
|
return NextResponse.next()
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionCookie = getSessionCookie(request, { cookiePrefix: "taxhacker" })
|
const sessionCookie = getSessionCookie(request, { cookiePrefix: "taxhacker" })
|
||||||
if (!sessionCookie) {
|
if (!sessionCookie) {
|
||||||
return NextResponse.redirect(new URL(AUTH_LOGIN_URL, request.url))
|
return NextResponse.redirect(new URL(globalConfig.auth.loginUrl, request.url))
|
||||||
}
|
}
|
||||||
return NextResponse.next()
|
return NextResponse.next()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { prisma } from "@/lib/db"
|
|||||||
type BackupSetting = {
|
type BackupSetting = {
|
||||||
filename: string
|
filename: string
|
||||||
model: any
|
model: any
|
||||||
recordToBackup: (userId: string, row: any) => Record<string, any>
|
backup: (userId: string, row: any) => Record<string, any>
|
||||||
backupToRecord: (userId: string, json: Record<string, any>) => any
|
restore: (userId: string, json: Record<string, any>) => any
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ordering is important here
|
// Ordering is important here
|
||||||
@@ -12,7 +12,7 @@ export const MODEL_BACKUP: BackupSetting[] = [
|
|||||||
{
|
{
|
||||||
filename: "settings.json",
|
filename: "settings.json",
|
||||||
model: prisma.setting,
|
model: prisma.setting,
|
||||||
recordToBackup: (userId: string, row: any) => {
|
backup: (userId: string, row: any) => {
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
code: row.code,
|
code: row.code,
|
||||||
@@ -21,9 +21,8 @@ export const MODEL_BACKUP: BackupSetting[] = [
|
|||||||
value: row.value,
|
value: row.value,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
backupToRecord: (userId: string, json: any) => {
|
restore: (userId: string, json: any) => {
|
||||||
return {
|
return {
|
||||||
id: json.id,
|
|
||||||
code: json.code,
|
code: json.code,
|
||||||
name: json.name,
|
name: json.name,
|
||||||
description: json.description,
|
description: json.description,
|
||||||
@@ -39,16 +38,15 @@ export const MODEL_BACKUP: BackupSetting[] = [
|
|||||||
{
|
{
|
||||||
filename: "currencies.json",
|
filename: "currencies.json",
|
||||||
model: prisma.currency,
|
model: prisma.currency,
|
||||||
recordToBackup: (userId: string, row: any) => {
|
backup: (userId: string, row: any) => {
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
code: row.code,
|
code: row.code,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
backupToRecord: (userId: string, json: any) => {
|
restore: (userId: string, json: any) => {
|
||||||
return {
|
return {
|
||||||
id: json.id,
|
|
||||||
code: json.code,
|
code: json.code,
|
||||||
name: json.name,
|
name: json.name,
|
||||||
user: {
|
user: {
|
||||||
@@ -62,7 +60,7 @@ export const MODEL_BACKUP: BackupSetting[] = [
|
|||||||
{
|
{
|
||||||
filename: "categories.json",
|
filename: "categories.json",
|
||||||
model: prisma.category,
|
model: prisma.category,
|
||||||
recordToBackup: (userId: string, row: any) => {
|
backup: (userId: string, row: any) => {
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
code: row.code,
|
code: row.code,
|
||||||
@@ -72,9 +70,8 @@ export const MODEL_BACKUP: BackupSetting[] = [
|
|||||||
createdAt: row.createdAt,
|
createdAt: row.createdAt,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
backupToRecord: (userId: string, json: any) => {
|
restore: (userId: string, json: any) => {
|
||||||
return {
|
return {
|
||||||
id: json.id,
|
|
||||||
code: json.code,
|
code: json.code,
|
||||||
name: json.name,
|
name: json.name,
|
||||||
color: json.color,
|
color: json.color,
|
||||||
@@ -91,7 +88,7 @@ export const MODEL_BACKUP: BackupSetting[] = [
|
|||||||
{
|
{
|
||||||
filename: "projects.json",
|
filename: "projects.json",
|
||||||
model: prisma.project,
|
model: prisma.project,
|
||||||
recordToBackup: (userId: string, row: any) => {
|
backup: (userId: string, row: any) => {
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
code: row.code,
|
code: row.code,
|
||||||
@@ -101,9 +98,8 @@ export const MODEL_BACKUP: BackupSetting[] = [
|
|||||||
createdAt: row.createdAt,
|
createdAt: row.createdAt,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
backupToRecord: (userId: string, json: any) => {
|
restore: (userId: string, json: any) => {
|
||||||
return {
|
return {
|
||||||
id: json.id,
|
|
||||||
code: json.code,
|
code: json.code,
|
||||||
name: json.name,
|
name: json.name,
|
||||||
color: json.color,
|
color: json.color,
|
||||||
@@ -120,7 +116,7 @@ export const MODEL_BACKUP: BackupSetting[] = [
|
|||||||
{
|
{
|
||||||
filename: "fields.json",
|
filename: "fields.json",
|
||||||
model: prisma.field,
|
model: prisma.field,
|
||||||
recordToBackup: (userId: string, row: any) => {
|
backup: (userId: string, row: any) => {
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
code: row.code,
|
code: row.code,
|
||||||
@@ -134,9 +130,8 @@ export const MODEL_BACKUP: BackupSetting[] = [
|
|||||||
isExtra: row.isExtra,
|
isExtra: row.isExtra,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
backupToRecord: (userId: string, json: any) => {
|
restore: (userId: string, json: any) => {
|
||||||
return {
|
return {
|
||||||
id: json.id,
|
|
||||||
code: json.code,
|
code: json.code,
|
||||||
name: json.name,
|
name: json.name,
|
||||||
type: json.type,
|
type: json.type,
|
||||||
@@ -157,7 +152,7 @@ export const MODEL_BACKUP: BackupSetting[] = [
|
|||||||
{
|
{
|
||||||
filename: "files.json",
|
filename: "files.json",
|
||||||
model: prisma.file,
|
model: prisma.file,
|
||||||
recordToBackup: (userId: string, row: any) => {
|
backup: (userId: string, row: any) => {
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
filename: row.filename,
|
filename: row.filename,
|
||||||
@@ -168,7 +163,7 @@ export const MODEL_BACKUP: BackupSetting[] = [
|
|||||||
createdAt: row.createdAt,
|
createdAt: row.createdAt,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
backupToRecord: (userId: string, json: any) => {
|
restore: (userId: string, json: any) => {
|
||||||
return {
|
return {
|
||||||
id: json.id,
|
id: json.id,
|
||||||
filename: json.filename,
|
filename: json.filename,
|
||||||
@@ -187,7 +182,7 @@ export const MODEL_BACKUP: BackupSetting[] = [
|
|||||||
{
|
{
|
||||||
filename: "transactions.json",
|
filename: "transactions.json",
|
||||||
model: prisma.transaction,
|
model: prisma.transaction,
|
||||||
recordToBackup: (userId: string, row: any) => {
|
backup: (userId: string, row: any) => {
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
@@ -209,7 +204,7 @@ export const MODEL_BACKUP: BackupSetting[] = [
|
|||||||
text: row.text,
|
text: row.text,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
backupToRecord: (userId: string, json: any) => {
|
restore: (userId: string, json: any) => {
|
||||||
return {
|
return {
|
||||||
id: json.id,
|
id: json.id,
|
||||||
name: json.name,
|
name: json.name,
|
||||||
@@ -244,21 +239,25 @@ export const MODEL_BACKUP: BackupSetting[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export async function modelToJSON(userId: string, backup: BackupSetting): Promise<string> {
|
export async function modelToJSON(userId: string, backupSettings: BackupSetting): Promise<string> {
|
||||||
const data = await backup.model.findMany({ where: { userId } })
|
const data = await backupSettings.model.findMany({ where: { userId } })
|
||||||
|
|
||||||
if (!data || data.length === 0) {
|
if (!data || data.length === 0) {
|
||||||
return "[]"
|
return "[]"
|
||||||
}
|
}
|
||||||
|
|
||||||
return JSON.stringify(
|
return JSON.stringify(
|
||||||
data.map((row: any) => backup.recordToBackup(userId, row)),
|
data.map((row: any) => backupSettings.backup(userId, row)),
|
||||||
null,
|
null,
|
||||||
2
|
2
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function modelFromJSON(userId: string, backup: BackupSetting, jsonContent: string): Promise<number> {
|
export async function modelFromJSON(
|
||||||
|
userId: string,
|
||||||
|
backupSettings: BackupSetting,
|
||||||
|
jsonContent: string
|
||||||
|
): Promise<number> {
|
||||||
if (!jsonContent) return 0
|
if (!jsonContent) return 0
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -273,8 +272,8 @@ export async function modelFromJSON(userId: string, backup: BackupSetting, jsonC
|
|||||||
const record = preprocessRowData(rawRecord)
|
const record = preprocessRowData(rawRecord)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await backup.backupToRecord(userId, record)
|
const data = await backupSettings.restore(userId, record)
|
||||||
await backup.model.create({ data })
|
await backupSettings.model.create({ data })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error importing record:`, error)
|
console.error(`Error importing record:`, error)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user