feat: config.js

This commit is contained in:
Vasily Zubarev
2025-04-03 14:36:16 +02:00
parent f523b1f8ba
commit f1a26e511e
26 changed files with 165 additions and 126 deletions

View File

@@ -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> }) {

View File

@@ -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>

View File

@@ -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>
</>
)

View File

@@ -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)

View File

@@ -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">

View File

@@ -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 (

View File

@@ -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 (

View File

@@ -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)) {

View File

@@ -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>
)

View File

@@ -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,
})

View File

@@ -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",

View File

@@ -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 />
}

View File

@@ -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">

View File

@@ -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}
/>

View File

@@ -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

View File

@@ -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"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
View 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

View File

@@ -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"

View File

@@ -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}`
)}`

View File

@@ -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,

View File

@@ -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()
}

View File

@@ -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)
}