mirror of
https://github.com/vas3k/TaxHacker.git
synced 2025-12-21 04:50:12 -06: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 { Separator } from "@/components/ui/separator"
|
||||
import { getCurrentUser } from "@/lib/auth"
|
||||
import { APP_DESCRIPTION } from "@/lib/constants"
|
||||
import config from "@/lib/config"
|
||||
import { getUnsortedFiles } from "@/models/files"
|
||||
import { getSettings } from "@/models/settings"
|
||||
import { TransactionFilters } from "@/models/transactions"
|
||||
@@ -12,7 +12,7 @@ import { Metadata } from "next"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Dashboard",
|
||||
description: APP_DESCRIPTION,
|
||||
description: config.app.description,
|
||||
}
|
||||
|
||||
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 { Toaster } from "@/components/ui/sonner"
|
||||
import { getCurrentUser } from "@/lib/auth"
|
||||
import { APP_DESCRIPTION, APP_TITLE } from "@/lib/constants"
|
||||
import config from "@/lib/config"
|
||||
import { getUnsortedFilesCount } from "@/models/files"
|
||||
import type { Metadata, Viewport } from "next"
|
||||
import "../globals.css"
|
||||
@@ -13,9 +13,9 @@ import { NotificationProvider } from "./context"
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
template: "%s | TaxHacker",
|
||||
default: APP_TITLE,
|
||||
default: config.app.title,
|
||||
},
|
||||
description: APP_DESCRIPTION,
|
||||
description: config.app.description,
|
||||
icons: {
|
||||
icon: "/favicon.ico",
|
||||
shortcut: "/favicon.ico",
|
||||
@@ -38,13 +38,14 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
<SidebarProvider>
|
||||
<MobileMenu unsortedFilesCount={unsortedFilesCount} />
|
||||
<AppSidebar
|
||||
unsortedFilesCount={unsortedFilesCount}
|
||||
profile={{
|
||||
id: user.id,
|
||||
name: user.name || "",
|
||||
email: user.email,
|
||||
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>
|
||||
</SidebarProvider>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import LLMSettingsForm from "@/components/settings/llm-settings-form"
|
||||
import { getCurrentUser } from "@/lib/auth"
|
||||
import config from "@/lib/config"
|
||||
import { getFields } from "@/models/fields"
|
||||
import { getSettings } from "@/models/settings"
|
||||
|
||||
@@ -11,7 +12,7 @@ export default async function LlmSettingsPage() {
|
||||
return (
|
||||
<>
|
||||
<div className="w-full max-w-2xl">
|
||||
<LLMSettingsForm settings={settings} fields={fields} />
|
||||
<LLMSettingsForm settings={settings} fields={fields} showApiKey={config.selfHosted.isEnabled} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ import { buildLLMPrompt } from "@/ai/prompt"
|
||||
import { fieldsToJsonSchema } from "@/ai/schema"
|
||||
import { transactionFormSchema } from "@/forms/transactions"
|
||||
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 { DEFAULT_PROMPT_ANALYSE_NEW_FILE } from "@/models/defaults"
|
||||
import { deleteFile, getFileById, updateFile } from "@/models/files"
|
||||
@@ -50,7 +50,7 @@ export async function analyzeFileAction(
|
||||
prompt,
|
||||
schema,
|
||||
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)
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import AnalyzeForm from "@/components/unsorted/analyze-form"
|
||||
import { getCurrentUser } from "@/lib/auth"
|
||||
import { IS_SELF_HOSTED_MODE } from "@/lib/constants"
|
||||
import config from "@/lib/config"
|
||||
import { getCategories } from "@/models/categories"
|
||||
import { getCurrencies } from "@/models/currencies"
|
||||
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>
|
||||
</header>
|
||||
|
||||
{IS_SELF_HOSTED_MODE && !settings.openai_api_key && (
|
||||
{config.selfHosted.isEnabled && !settings.openai_api_key && (
|
||||
<Alert>
|
||||
<Settings className="h-4 w-4 mt-2" />
|
||||
<div className="flex flex-row justify-between pt-2">
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { LoginForm } from "@/components/auth/login-form"
|
||||
import { Card, CardContent, CardTitle } from "@/components/ui/card"
|
||||
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"
|
||||
|
||||
export default async function LoginPage() {
|
||||
if (IS_SELF_HOSTED_MODE) {
|
||||
redirect(SELF_HOSTED_REDIRECT_URL)
|
||||
if (config.selfHosted.isEnabled) {
|
||||
redirect(config.selfHosted.redirectUrl)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,7 +3,7 @@ import { FormInput } from "@/components/forms/simple"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardDescription, CardTitle } from "@/components/ui/card"
|
||||
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 { getSelfHostedUser } from "@/models/users"
|
||||
import { ShieldAlert } from "lucide-react"
|
||||
@@ -11,7 +11,7 @@ import { redirect } from "next/navigation"
|
||||
import { selfHostedGetStartedAction } from "../actions"
|
||||
|
||||
export default async function SelfHostedWelcomePage() {
|
||||
if (!IS_SELF_HOSTED_MODE) {
|
||||
if (!config.selfHosted.isEnabled) {
|
||||
return (
|
||||
<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">
|
||||
@@ -31,7 +31,7 @@ export default async function SelfHostedWelcomePage() {
|
||||
|
||||
const user = await getSelfHostedUser()
|
||||
if (user) {
|
||||
redirect(SELF_HOSTED_REDIRECT_URL)
|
||||
redirect(config.selfHosted.redirectUrl)
|
||||
}
|
||||
|
||||
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 { getSelfHostedUser } from "@/models/users"
|
||||
import { revalidatePath } from "next/cache"
|
||||
import { redirect } from "next/navigation"
|
||||
|
||||
export async function GET(request: Request) {
|
||||
if (!IS_SELF_HOSTED_MODE) {
|
||||
redirect(AUTH_LOGIN_URL)
|
||||
if (!config.selfHosted.isEnabled) {
|
||||
redirect(config.auth.loginUrl)
|
||||
}
|
||||
|
||||
const user = await getSelfHostedUser()
|
||||
if (!user) {
|
||||
redirect(SELF_HOSTED_WELCOME_URL)
|
||||
redirect(config.selfHosted.welcomeUrl)
|
||||
}
|
||||
|
||||
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 { 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"
|
||||
|
||||
export default async function LoginPage() {
|
||||
if (IS_SELF_HOSTED_MODE) {
|
||||
redirect(SELF_HOSTED_REDIRECT_URL)
|
||||
if (config.selfHosted.isEnabled) {
|
||||
redirect(config.selfHosted.redirectUrl)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -15,10 +16,13 @@ export default async function LoginPage() {
|
||||
<ColoredText>TaxHacker: Cloud Edition</ColoredText>
|
||||
</CardTitle>
|
||||
<CardContent className="w-full">
|
||||
<div className="text-center text-md text-muted-foreground">
|
||||
Creating new account is disabled for now. Please use the self-hosted version.
|
||||
</div>
|
||||
{/* <SignupForm /> */}
|
||||
{config.auth.disableSignup ? (
|
||||
<div className="text-center text-md text-muted-foreground">
|
||||
Creating new account is disabled for now. Please use the self-hosted version.
|
||||
</div>
|
||||
) : (
|
||||
<SignupForm />
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use server"
|
||||
|
||||
import config from "@/lib/config"
|
||||
import { resend, sendNewsletterWelcomeEmail } from "@/lib/email"
|
||||
|
||||
export async function subscribeToNewsletterAction(email: string) {
|
||||
@@ -9,7 +10,7 @@ export async function subscribeToNewsletterAction(email: string) {
|
||||
}
|
||||
|
||||
const existingContacts = await resend.contacts.list({
|
||||
audienceId: process.env.RESEND_AUDIENCE_ID as string,
|
||||
audienceId: config.email.audienceId,
|
||||
})
|
||||
|
||||
if (existingContacts.data) {
|
||||
@@ -22,7 +23,7 @@ export async function subscribeToNewsletterAction(email: string) {
|
||||
|
||||
await resend.contacts.create({
|
||||
email,
|
||||
audienceId: process.env.RESEND_AUDIENCE_ID as string,
|
||||
audienceId: config.email.audienceId,
|
||||
unsubscribed: false,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import config from "@/lib/config"
|
||||
import type { Metadata, Viewport } from "next"
|
||||
import "./globals.css"
|
||||
import { APP_DESCRIPTION, APP_TITLE } from "@/lib/constants"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
template: "%s | TaxHacker",
|
||||
default: APP_TITLE,
|
||||
default: config.app.title,
|
||||
},
|
||||
description: APP_DESCRIPTION,
|
||||
description: config.app.description,
|
||||
icons: {
|
||||
icon: "/favicon.ico",
|
||||
shortcut: "/favicon.ico",
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import LandingPage from "@/app/landing/landing"
|
||||
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"
|
||||
|
||||
export default async function Home() {
|
||||
const session = await getSession()
|
||||
if (!session) {
|
||||
if (IS_SELF_HOSTED_MODE) {
|
||||
redirect(SELF_HOSTED_REDIRECT_URL)
|
||||
if (config.selfHosted.isEnabled) {
|
||||
redirect(config.selfHosted.redirectUrl)
|
||||
}
|
||||
return <LandingPage />
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useNotification } from "@/app/(app)/context"
|
||||
import { uploadFilesAction } from "@/app/(app)/files/actions"
|
||||
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 { useRouter } from "next/navigation"
|
||||
import { startTransition, useState } from "react"
|
||||
@@ -48,7 +48,7 @@ export default function DashboardDropZoneWidget() {
|
||||
id="fileInput"
|
||||
className="hidden"
|
||||
multiple
|
||||
accept={FILE_ACCEPTED_MIMETYPES}
|
||||
accept={config.upload.acceptedMimeTypes}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<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 { uploadFilesAction } from "@/app/(app)/files/actions"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { FILE_ACCEPTED_MIMETYPES } from "@/lib/constants"
|
||||
import config from "@/lib/config"
|
||||
import { Loader2 } from "lucide-react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { ComponentProps, startTransition, useRef, useState } from "react"
|
||||
@@ -54,7 +54,7 @@ export function UploadButton({ children, ...props }: { children: React.ReactNode
|
||||
id="fileInput"
|
||||
className="hidden"
|
||||
multiple
|
||||
accept={FILE_ACCEPTED_MIMETYPES}
|
||||
accept={config.upload.acceptedMimeTypes}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
|
||||
|
||||
@@ -6,29 +6,40 @@ import { FormError } from "@/components/forms/error"
|
||||
import { FormInput, FormTextarea } from "@/components/forms/simple"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardTitle } from "@/components/ui/card"
|
||||
import { IS_SELF_HOSTED_MODE } from "@/lib/constants"
|
||||
import { Field } from "@prisma/client"
|
||||
import { CircleCheckBig, Edit } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
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)
|
||||
|
||||
return (
|
||||
<>
|
||||
<form action={saveAction} className="space-y-4">
|
||||
{IS_SELF_HOSTED_MODE && (
|
||||
<FormInput title="OpenAI API Key" name="openai_api_key" defaultValue={settings.openai_api_key} />
|
||||
)}
|
||||
{showApiKey && (
|
||||
<>
|
||||
<FormInput title="OpenAI API Key" name="openai_api_key" defaultValue={settings.openai_api_key} />
|
||||
|
||||
{IS_SELF_HOSTED_MODE && (
|
||||
<small className="text-muted-foreground">
|
||||
Get your API key from{" "}
|
||||
<a href="https://platform.openai.com/settings/organization/api-keys" target="_blank" className="underline">
|
||||
OpenAI Platform Console
|
||||
</a>
|
||||
</small>
|
||||
<small className="text-muted-foreground">
|
||||
Get your API key from{" "}
|
||||
<a
|
||||
href="https://platform.openai.com/settings/organization/api-keys"
|
||||
target="_blank"
|
||||
className="underline"
|
||||
>
|
||||
OpenAI Platform Console
|
||||
</a>
|
||||
</small>
|
||||
</>
|
||||
)}
|
||||
|
||||
<FormTextarea
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { useSidebar } from "@/components/ui/sidebar"
|
||||
import { APP_TITLE } from "@/lib/constants"
|
||||
import config from "@/lib/config"
|
||||
import Link from "next/link"
|
||||
|
||||
export default function MobileMenu({ unsortedFilesCount }: { unsortedFilesCount: number }) {
|
||||
@@ -15,7 +15,7 @@ export default function MobileMenu({ unsortedFilesCount }: { unsortedFilesCount:
|
||||
<AvatarFallback className="rounded-lg">AI</AvatarFallback>
|
||||
</Avatar>
|
||||
<Link href="/" className="text-lg font-bold">
|
||||
{APP_TITLE}
|
||||
{config.app.title}
|
||||
</Link>
|
||||
<Link
|
||||
href="/unsorted"
|
||||
|
||||
@@ -10,12 +10,11 @@ import {
|
||||
import { SidebarMenuButton } from "@/components/ui/sidebar"
|
||||
import { UserProfile } from "@/lib/auth"
|
||||
import { authClient } from "@/lib/auth-client"
|
||||
import { IS_SELF_HOSTED_MODE } from "@/lib/constants"
|
||||
import { LogOut, MoreVertical, User } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
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 () => {
|
||||
await authClient.signOut({})
|
||||
redirect("/")
|
||||
@@ -61,14 +60,16 @@ export default function SidebarUser({ profile }: { profile: UserProfile }) {
|
||||
</Link>
|
||||
</DropdownMenuItem> */}
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
{!IS_SELF_HOSTED_MODE && (
|
||||
<DropdownMenuItem asChild>
|
||||
<span onClick={signOut} className="flex items-center gap-2 text-red-600 cursor-pointer">
|
||||
<LogOut className="h-4 w-4" />
|
||||
Log out
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
{!isSelfHosted && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<span onClick={signOut} className="flex items-center gap-2 text-red-600 cursor-pointer">
|
||||
<LogOut className="h-4 w-4" />
|
||||
Log out
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar"
|
||||
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 Image from "next/image"
|
||||
import Link from "next/link"
|
||||
@@ -28,7 +28,15 @@ import { Blinker } from "./blinker"
|
||||
import { SidebarMenuItemWithHighlight } from "./sidebar-item"
|
||||
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 pathname = usePathname()
|
||||
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} />
|
||||
<div className="grid flex-1 text-left leading-tight">
|
||||
<span className="truncate font-semibold text-lg">
|
||||
<ColoredText>{APP_TITLE}</ColoredText>
|
||||
<ColoredText>{config.app.title}</ColoredText>
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
@@ -124,7 +132,7 @@ export function AppSidebar({ unsortedFilesCount, profile }: { unsortedFilesCount
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
{IS_SELF_HOSTED_MODE && (
|
||||
{isSelfHosted && (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href="https://vas3k.com/donate/" target="_blank">
|
||||
@@ -146,7 +154,7 @@ export function AppSidebar({ unsortedFilesCount, profile }: { unsortedFilesCount
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarUser profile={profile} />
|
||||
<SidebarUser profile={profile} isSelfHosted={isSelfHosted} />
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { deleteTransactionFileAction, uploadTransactionFilesAction } from "@/app
|
||||
import { FilePreview } from "@/components/files/preview"
|
||||
import { Button } from "@/components/ui/button"
|
||||
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 { Loader2, Upload, X } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
@@ -72,7 +72,7 @@ export default function TransactionFiles({ transaction, files }: { transaction:
|
||||
name="file"
|
||||
className="absolute inset-0 top-0 left-0 w-full h-full opacity-0"
|
||||
onChange={handleFileChange}
|
||||
accept={FILE_ACCEPTED_MIMETYPES}
|
||||
accept={config.upload.acceptedMimeTypes}
|
||||
/>
|
||||
</label>
|
||||
</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 { getSelfHostedUser, getUserByEmail } from "@/models/users"
|
||||
import { getSelfHostedUser } from "@/models/users"
|
||||
import { User } from "@prisma/client"
|
||||
import { betterAuth } from "better-auth"
|
||||
import { prismaAdapter } from "better-auth/adapters/prisma"
|
||||
@@ -22,7 +22,7 @@ export const auth = betterAuth({
|
||||
database: prismaAdapter(prisma, { provider: "postgresql" }),
|
||||
email: {
|
||||
provider: "resend",
|
||||
from: process.env.RESEND_FROM_EMAIL!,
|
||||
from: config.email.from,
|
||||
resend,
|
||||
},
|
||||
session: {
|
||||
@@ -49,14 +49,10 @@ export const auth = betterAuth({
|
||||
},
|
||||
plugins: [
|
||||
emailOTP({
|
||||
disableSignUp: true,
|
||||
disableSignUp: config.auth.disableSignup,
|
||||
otpLength: 6,
|
||||
expiresIn: 10 * 60, // 10 minutes
|
||||
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 })
|
||||
},
|
||||
}),
|
||||
@@ -65,7 +61,7 @@ export const auth = betterAuth({
|
||||
})
|
||||
|
||||
export async function getSession() {
|
||||
if (IS_SELF_HOSTED_MODE) {
|
||||
if (config.selfHosted.isEnabled) {
|
||||
const user = await getSelfHostedUser()
|
||||
return user ? { user } : null
|
||||
}
|
||||
@@ -78,10 +74,10 @@ export async function getSession() {
|
||||
export async function getCurrentUser(): Promise<User> {
|
||||
const session = await getSession()
|
||||
if (!session || !session.user) {
|
||||
if (IS_SELF_HOSTED_MODE) {
|
||||
redirect(SELF_HOSTED_REDIRECT_URL)
|
||||
if (config.selfHosted.isEnabled) {
|
||||
redirect(config.selfHosted.redirectUrl)
|
||||
} else {
|
||||
redirect(AUTH_LOGIN_URL)
|
||||
redirect(config.auth.loginUrl)
|
||||
}
|
||||
}
|
||||
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[]> {
|
||||
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(
|
||||
`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 React from "react"
|
||||
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 }) {
|
||||
const html = React.createElement(OTPEmail, { otp })
|
||||
|
||||
return await resend.emails.send({
|
||||
from: process.env.RESEND_FROM_EMAIL!,
|
||||
from: config.email.from,
|
||||
to: email,
|
||||
subject: "Your TaxHacker verification code",
|
||||
react: html,
|
||||
@@ -20,7 +21,7 @@ export async function sendNewsletterWelcomeEmail(email: string) {
|
||||
const html = React.createElement(NewsletterWelcomeEmail)
|
||||
|
||||
return await resend.emails.send({
|
||||
from: process.env.RESEND_FROM_EMAIL as string,
|
||||
from: config.email.from,
|
||||
to: email,
|
||||
subject: "Welcome to TaxHacker Newsletter!",
|
||||
react: html,
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { default as globalConfig } from "@/lib/config"
|
||||
import { getSessionCookie } from "better-auth/cookies"
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { AUTH_LOGIN_URL, IS_SELF_HOSTED_MODE } from "./lib/constants"
|
||||
|
||||
export default async function middleware(request: NextRequest) {
|
||||
if (IS_SELF_HOSTED_MODE) {
|
||||
if (globalConfig.selfHosted.isEnabled) {
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
const sessionCookie = getSessionCookie(request, { cookiePrefix: "taxhacker" })
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { prisma } from "@/lib/db"
|
||||
type BackupSetting = {
|
||||
filename: string
|
||||
model: any
|
||||
recordToBackup: (userId: string, row: any) => Record<string, any>
|
||||
backupToRecord: (userId: string, json: Record<string, any>) => any
|
||||
backup: (userId: string, row: any) => Record<string, any>
|
||||
restore: (userId: string, json: Record<string, any>) => any
|
||||
}
|
||||
|
||||
// Ordering is important here
|
||||
@@ -12,7 +12,7 @@ export const MODEL_BACKUP: BackupSetting[] = [
|
||||
{
|
||||
filename: "settings.json",
|
||||
model: prisma.setting,
|
||||
recordToBackup: (userId: string, row: any) => {
|
||||
backup: (userId: string, row: any) => {
|
||||
return {
|
||||
id: row.id,
|
||||
code: row.code,
|
||||
@@ -21,9 +21,8 @@ export const MODEL_BACKUP: BackupSetting[] = [
|
||||
value: row.value,
|
||||
}
|
||||
},
|
||||
backupToRecord: (userId: string, json: any) => {
|
||||
restore: (userId: string, json: any) => {
|
||||
return {
|
||||
id: json.id,
|
||||
code: json.code,
|
||||
name: json.name,
|
||||
description: json.description,
|
||||
@@ -39,16 +38,15 @@ export const MODEL_BACKUP: BackupSetting[] = [
|
||||
{
|
||||
filename: "currencies.json",
|
||||
model: prisma.currency,
|
||||
recordToBackup: (userId: string, row: any) => {
|
||||
backup: (userId: string, row: any) => {
|
||||
return {
|
||||
id: row.id,
|
||||
code: row.code,
|
||||
name: row.name,
|
||||
}
|
||||
},
|
||||
backupToRecord: (userId: string, json: any) => {
|
||||
restore: (userId: string, json: any) => {
|
||||
return {
|
||||
id: json.id,
|
||||
code: json.code,
|
||||
name: json.name,
|
||||
user: {
|
||||
@@ -62,7 +60,7 @@ export const MODEL_BACKUP: BackupSetting[] = [
|
||||
{
|
||||
filename: "categories.json",
|
||||
model: prisma.category,
|
||||
recordToBackup: (userId: string, row: any) => {
|
||||
backup: (userId: string, row: any) => {
|
||||
return {
|
||||
id: row.id,
|
||||
code: row.code,
|
||||
@@ -72,9 +70,8 @@ export const MODEL_BACKUP: BackupSetting[] = [
|
||||
createdAt: row.createdAt,
|
||||
}
|
||||
},
|
||||
backupToRecord: (userId: string, json: any) => {
|
||||
restore: (userId: string, json: any) => {
|
||||
return {
|
||||
id: json.id,
|
||||
code: json.code,
|
||||
name: json.name,
|
||||
color: json.color,
|
||||
@@ -91,7 +88,7 @@ export const MODEL_BACKUP: BackupSetting[] = [
|
||||
{
|
||||
filename: "projects.json",
|
||||
model: prisma.project,
|
||||
recordToBackup: (userId: string, row: any) => {
|
||||
backup: (userId: string, row: any) => {
|
||||
return {
|
||||
id: row.id,
|
||||
code: row.code,
|
||||
@@ -101,9 +98,8 @@ export const MODEL_BACKUP: BackupSetting[] = [
|
||||
createdAt: row.createdAt,
|
||||
}
|
||||
},
|
||||
backupToRecord: (userId: string, json: any) => {
|
||||
restore: (userId: string, json: any) => {
|
||||
return {
|
||||
id: json.id,
|
||||
code: json.code,
|
||||
name: json.name,
|
||||
color: json.color,
|
||||
@@ -120,7 +116,7 @@ export const MODEL_BACKUP: BackupSetting[] = [
|
||||
{
|
||||
filename: "fields.json",
|
||||
model: prisma.field,
|
||||
recordToBackup: (userId: string, row: any) => {
|
||||
backup: (userId: string, row: any) => {
|
||||
return {
|
||||
id: row.id,
|
||||
code: row.code,
|
||||
@@ -134,9 +130,8 @@ export const MODEL_BACKUP: BackupSetting[] = [
|
||||
isExtra: row.isExtra,
|
||||
}
|
||||
},
|
||||
backupToRecord: (userId: string, json: any) => {
|
||||
restore: (userId: string, json: any) => {
|
||||
return {
|
||||
id: json.id,
|
||||
code: json.code,
|
||||
name: json.name,
|
||||
type: json.type,
|
||||
@@ -157,7 +152,7 @@ export const MODEL_BACKUP: BackupSetting[] = [
|
||||
{
|
||||
filename: "files.json",
|
||||
model: prisma.file,
|
||||
recordToBackup: (userId: string, row: any) => {
|
||||
backup: (userId: string, row: any) => {
|
||||
return {
|
||||
id: row.id,
|
||||
filename: row.filename,
|
||||
@@ -168,7 +163,7 @@ export const MODEL_BACKUP: BackupSetting[] = [
|
||||
createdAt: row.createdAt,
|
||||
}
|
||||
},
|
||||
backupToRecord: (userId: string, json: any) => {
|
||||
restore: (userId: string, json: any) => {
|
||||
return {
|
||||
id: json.id,
|
||||
filename: json.filename,
|
||||
@@ -187,7 +182,7 @@ export const MODEL_BACKUP: BackupSetting[] = [
|
||||
{
|
||||
filename: "transactions.json",
|
||||
model: prisma.transaction,
|
||||
recordToBackup: (userId: string, row: any) => {
|
||||
backup: (userId: string, row: any) => {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
@@ -209,7 +204,7 @@ export const MODEL_BACKUP: BackupSetting[] = [
|
||||
text: row.text,
|
||||
}
|
||||
},
|
||||
backupToRecord: (userId: string, json: any) => {
|
||||
restore: (userId: string, json: any) => {
|
||||
return {
|
||||
id: json.id,
|
||||
name: json.name,
|
||||
@@ -244,21 +239,25 @@ export const MODEL_BACKUP: BackupSetting[] = [
|
||||
},
|
||||
]
|
||||
|
||||
export async function modelToJSON(userId: string, backup: BackupSetting): Promise<string> {
|
||||
const data = await backup.model.findMany({ where: { userId } })
|
||||
export async function modelToJSON(userId: string, backupSettings: BackupSetting): Promise<string> {
|
||||
const data = await backupSettings.model.findMany({ where: { userId } })
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return "[]"
|
||||
}
|
||||
|
||||
return JSON.stringify(
|
||||
data.map((row: any) => backup.recordToBackup(userId, row)),
|
||||
data.map((row: any) => backupSettings.backup(userId, row)),
|
||||
null,
|
||||
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
|
||||
|
||||
try {
|
||||
@@ -273,8 +272,8 @@ export async function modelFromJSON(userId: string, backup: BackupSetting, jsonC
|
||||
const record = preprocessRowData(rawRecord)
|
||||
|
||||
try {
|
||||
const data = await backup.backupToRecord(userId, record)
|
||||
await backup.model.create({ data })
|
||||
const data = await backupSettings.restore(userId, record)
|
||||
await backupSettings.model.create({ data })
|
||||
} catch (error) {
|
||||
console.error(`Error importing record:`, error)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user