Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions backend/src/external/quickbooks/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import {
const PROD_BASE_URL = "https://quickbooks.api.intuit.com";
const SANDBOX_BASE_URL = "https://sandbox-quickbooks.api.intuit.com";

const PROD_PRISERE_API_URL = process.env.PROD_PRISERE_API_URL;
const DEV_PRISERE_API_URL = process.env.DEV_PRISERE_API_URL;

export const QB_SCOPES = {
accounting: "com.intuit.quickbooks.accounting",
payment: "com.intuit.quickbooks.payment",
Expand Down Expand Up @@ -71,11 +74,9 @@ export class QuickbooksClient implements IQuickbooksClient {
this.BASE_URL = environment === "production" ? PROD_BASE_URL : SANDBOX_BASE_URL;
this.redirectUri =
environment === "production"
? // TODO: get a real redirect for prod
""
: // TODO: finalize if this is the real route we want to redirect to, I think we need a better frontend page or something,
// maybe we redirect from the server after going here?
"http://localhost:3001/quickbooks/redirect";
? // Note: Quickbooks Redirect URI was updated to match these URLs:
`${PROD_PRISERE_API_URL}/quickbooks/redirect`
: `${DEV_PRISERE_API_URL}/quickbooks/redirect`;
}

public generateUrl({ scopes }: { scopes: (keyof typeof QB_SCOPES)[] }) {
Expand Down
7 changes: 6 additions & 1 deletion backend/src/modules/company/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,12 @@ export class CompanyTransaction implements ICompanyTransaction {
}

try {
const result: Company | null = await this.db.getRepository(Company).findOneBy({ id: payload.id });
const result: Company | null = await this.db.getRepository(Company).findOne({
where: { id: payload.id },
relations: {
externals: true,
},
});

return result;
} catch (error) {
Expand Down
26 changes: 13 additions & 13 deletions backend/src/modules/openapi/quickbooks-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const addOpenApiQBRoutes = (openApi: OpenAPIHono, db: DataSource): OpenAP
const client = new QuickbooksClient({
clientId: process.env.QUICKBOOKS_CLIENT_ID!,
clientSecret: process.env.QUICKBOOKS_CLIENT_SECRET!,
environment: "sandbox",
environment: process.env.NODE_ENV === "production" ? "production" : "sandbox",
});

const service = new QuickbooksService(
Expand All @@ -48,8 +48,15 @@ const generateAuthRoute = createRoute({
summary: "Generates an OAuth URL for the user and redirects them",
tags: ["quickbooks"],
responses: {
302: {
description: "Redirected to QuickBooks OAuth url",
200: {
description: "Successfully redirected to quickbooks auth",
content: {
"application/json": {
schema: z.object({
url: z.string(),
}),
},
},
},
},
});
Expand All @@ -65,18 +72,11 @@ const generateSessionRoute = createRoute({
params: RedirectEndpointSuccessParams,
},
responses: {
200: {
description: "Successfully logged in through QB",
content: {
"application/json": {
schema: z.object({
success: z.literal(true),
}),
},
},
302: {
description: "Successfully authenticated, redirecting",
},
400: {
description: "Did not grant permissions",
description: "Authentication failed",
content: {
"application/json": {
schema: z.object({
Expand Down
10 changes: 4 additions & 6 deletions backend/src/modules/quickbooks/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@ import Boom from "@hapi/boom";
import { validate } from "uuid";

export interface IQuickbooksController {
redirectToAuthorization(ctx: Context): ControllerResponse<TypedResponse<undefined, 302>>;
generateSession(
ctx: Context
): ControllerResponse<TypedResponse<{ success: true }, 200> | TypedResponse<{ error: string }, 400>>;
redirectToAuthorization(ctx: Context): ControllerResponse<TypedResponse<{ url: string }, 200>>;
generateSession(ctx: Context): Promise<Response>;
updateUnprocessedInvoices(ctx: Context): ControllerResponse<TypedResponse<{ success: true }, 200>>;
importQuickbooksData(ctx: Context): ControllerResponse<TypedResponse<{ success: true }, 201>>;
}
Expand All @@ -22,7 +20,7 @@ export class QuickbooksController implements IQuickbooksController {

const { url } = await this.service.generateAuthUrl({ userId });

return ctx.redirect(url);
return ctx.json({ url }, 200);
}

async generateSession(ctx: Context) {
Expand All @@ -36,7 +34,7 @@ export class QuickbooksController implements IQuickbooksController {

await this.service.createQuickbooksSession(params);

return ctx.json({ success: true }, 200);
return ctx.redirect(`${process.env.FRONTEND_URL || "http://localhost:3000"}`);
}

async updateUnprocessedInvoices(ctx: Context) {
Expand Down
2 changes: 1 addition & 1 deletion backend/src/modules/quickbooks/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function quickbooksRoutes(db: DataSource) {
const client = new QuickbooksClient({
clientId: process.env.QUICKBOOKS_CLIENT_ID!,
clientSecret: process.env.QUICKBOOKS_CLIENT_SECRET!,
environment: "sandbox", // TODO: dev vs. prod
environment: process.env.NODE_ENV === "production" ? "production" : "sandbox",
});

const service = new QuickbooksService(
Expand Down
2 changes: 1 addition & 1 deletion backend/src/modules/quickbooks/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export class QuickbooksService implements IQuickbooksService {

if (!external) {
if (!maybeToken.initiatorUser.companyId) {
throw Boom.badRequest("The requesting user deos not belong to a company");
throw Boom.badRequest("The requesting user does not belong to a company");
}

await this.transaction.createCompanyRealm({
Expand Down
10 changes: 9 additions & 1 deletion backend/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,15 @@ const app = new Hono();

const apiPrefix = process.env.NODE_ENV === "production" ? "/prisere" : "/api/prisere";

app.use(`${apiPrefix}/*`, isAuthorized());
app.use(`${apiPrefix}/*`, async (ctx, next) => {
// Skip the middleware for the /quickbooks/redirect route
if (ctx.req.path === `${apiPrefix}/quickbooks/redirect`) {
return next();
}

// Apply the isAuthorized middleware for all other routes
return isAuthorized()(ctx, next);
});

setUpRoutes(app, AppDataSource, apiPrefix);

Expand Down
1 change: 1 addition & 0 deletions backend/terraform/INSTRUCTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export TF_VAR_ses_from_email=$(grep SES_FROM_EMAIL ../.env | cut -d '=' -f2 | tr
export TF_VAR_sqs_queue_url=$(grep SQS_QUEUE_URL_PROD ../.env | cut -d '=' -f2 | tr -d '"')
```
## Deploying the resources
NOTE: We can only have 2 Access key ID/secrets per account
- Only necesarry to do when the resources:
1) Dont exist in AWS (need to be created)
2) You need to update any AWS resources (It does not matter what you need to change/update)
Expand Down
11 changes: 9 additions & 2 deletions frontend/actions/auth.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"use server";

import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { createSupabaseClient } from "@/utils/supabase/server";
import { loginInitialState, requiredOnboardingProgress, signupInitialState } from "@/types/user";
import { createClient } from "@supabase/supabase-js";
import { importQuickbooksData } from "@/api/quickbooks";
import { getCompany } from "@/api/company";

export async function login(prevState: loginInitialState, formData: FormData) {
const supabase = await createSupabaseClient();
Expand All @@ -19,8 +20,14 @@ export async function login(prevState: loginInitialState, formData: FormData) {
message: error.message || "Login failed",
};
}

const company = await getCompany();
if (company?.externals) {
importQuickbooksData();
}

revalidatePath("/", "layout");
redirect("/");
return { success: true, message: "Login successful" };
}

export async function logoutUser() {
Expand Down
19 changes: 19 additions & 0 deletions frontend/api/quickbooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,22 @@ export const importQuickbooksData = async (): Promise<{ success: true } | undefi
};
return authWrapper<{ success: true } | undefined>()(req);
};

export const redirectToQuickbooks = async (): Promise<string | undefined> => {
const req = async (token: string): Promise<string | undefined> => {
const client = getClient();
const { data, response } = await client.GET("/quickbooks", {
headers: authHeader(token),
});

if (response.ok && data?.url) {
return data.url;
} else if (response.status === 401) {
console.log("Warning: Unauthorized access to QuickBooks");
} else {
console.log("Error: Unable to fetch QuickBooks URL");
}
};

return authWrapper<string | undefined>()(req);
};
9 changes: 0 additions & 9 deletions frontend/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"use client";
import { login } from "@/actions/auth";
import { getCompany } from "@/api/company";
import { importQuickbooksData } from "@/api/quickbooks";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
Expand Down Expand Up @@ -53,13 +51,6 @@ export default function LoginPage() {

useEffect(() => {
if (state?.success) {
// Start import in the background, do not wait before redirect happens
(async () => {
const company = await getCompany();
if (company?.externals) {
importQuickbooksData();
}
})();
redirect("/");
}
}, [state?.success]);
Expand Down
15 changes: 14 additions & 1 deletion frontend/app/profile/linked-accounts.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
"use client";
import { Button } from "@/components/ui/button";
import { QuickBooksIcon } from "@/icons/quickbooks";
import { CirclePlusIcon } from "lucide-react";
import { ProfileSettingsCard } from "./common";
import { redirectToQuickbooks } from "@/api/quickbooks";

export function LinkedAccountsSettings() {
const quickbooksAuth = async () => {
const url = await redirectToQuickbooks();
if (url) {
window.location.href = url;
} else {
console.error("Failed to retrieve QuickBooks URL");
}
};

return (
<ProfileSettingsCard
title="Linked Accounts"
Expand All @@ -17,7 +28,9 @@ export function LinkedAccountsSettings() {
<Button
className="bg-light-fuchsia hover:bg-light-fuchsia/80 text-fuchsia w-40 cursor-pointer"
size="sm"
// TODO: Connect up QB signin logic
onClick={async () => {
await quickbooksAuth();
}}
>
Connect account
</Button>
Expand Down
13 changes: 13 additions & 0 deletions frontend/app/signup/quickbooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,22 @@ import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { GoSync } from "react-icons/go";
import { FiUpload } from "react-icons/fi";
import { redirectToQuickbooks } from "@/api/quickbooks";

interface QuickbooksInfoProps {
handleNext: () => void;
}

export default function Quickbooks({ handleNext }: QuickbooksInfoProps) {
const quickbooksAuth = async () => {
const url = await redirectToQuickbooks();
if (url) {
window.location.href = url;
} else {
console.error("Failed to retrieve QuickBooks URL");
}
};

return (
<Card className="w-full px-[163px] py-[127px]">
<div className="flex justify-center">
Expand All @@ -27,6 +37,9 @@ export default function Quickbooks({ handleNext }: QuickbooksInfoProps) {
type="button"
className="max-h-[45px] w-fit bg-[var(--fuchsia)] text-white py-[12px] text-[16px]"
style={{ paddingInline: "20px" }}
onClick={async () => {
await quickbooksAuth();
}}
>
<GoSync /> Sync Quickbooks
</Button>
Expand Down
3 changes: 3 additions & 0 deletions frontend/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 18 additions & 5 deletions frontend/components/dashboard/NoDataPopup.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";

import { redirectToQuickbooks } from "@/api/quickbooks";
import { Button } from "@/components/ui/button";
import Link from "next/link";

Expand All @@ -11,6 +12,15 @@ type Props = {
export default function NoDataPopup({ isOpen, onClose }: Props) {
if (!isOpen) return <></>;

const quickbooksAuth = async () => {
const url = await redirectToQuickbooks();
if (url) {
window.open(url, "_blank");
} else {
console.error("Failed to retrieve QuickBooks URL");
}
};

return (
<div className="fixed inset-0 bg-black/30 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-8 max-w-md w-full mx-4 relative">
Expand All @@ -26,11 +36,14 @@ export default function NoDataPopup({ isOpen, onClose }: Props) {
</p>

<div className="flex gap-3">
<Link href="/quickbooks">
<Button className="rounded-full bg-fuchsia hover:bg-pink text-white px-6 w-42 h-16">
Sync Quickbooks
</Button>
</Link>
<Button
className="rounded-full bg-fuchsia hover:bg-pink text-white px-6 w-42 h-16"
onClick={async () => {
await quickbooksAuth();
}}
>
Sync Quickbooks
</Button>
<Link href="/upload-csv">
<Button variant="outline" className="rounded-full border-charcoal text-charcoal px-6 w-42 h-16">
Upload CSV
Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"react-dom": "19.1.0",
"react-dropzone": "^14.3.8",
"react-icons": "^5.5.0",
"react-switch": "^7.1.0",
"recharts": "2.15.4",
"tailwind-merge": "^3.3.1",
"vaul": "^1.1.2",
Expand Down
Loading
Loading