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
4 changes: 2 additions & 2 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ const bootstrap = async () => {

expressInstance.use("/api/payments/webhook", express.raw({ type: "application/json" }));

expressInstance.use(express.json());
expressInstance.use(express.urlencoded({ extended: true }));
expressInstance.use(express.json({ limit: "50mb" }));
expressInstance.use(express.urlencoded({ limit: "50mb", extended: true }));
expressInstance.use(passport.initialize());

expressInstance.get("/health", (req, res) => {
Expand Down
58 changes: 54 additions & 4 deletions backend/src/controllers/generative.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,26 @@ import logger from "../utils/logger";
import { Request, Response } from "express";
import { buildPlatformPrompt } from "../ai/prompts";
import { buildImageParts } from "../services/image.service";
import { IncreaseCaptionGenerations } from "../repositories/user.repository";
import { findUserByIdSafe, IncreaseCaptionGenerations } from "../repositories/user.repository";

export const generateCaptions = async (req: Request, res: Response) => {
try {
if (!req.auth) {
return new ErrorResponse("Unauthorized", { status: 401 }).send(res);
}

const user = await findUserByIdSafe(req.auth.id);
if (!user) {
return new ErrorResponse("User not found", { status: 404 }).send(res);
}

if (user.usage.captionGenerations >= user.limits.maxCaptionGenerations) {
return new ErrorResponse(
"You have reached your caption generation limit. Please upgrade your plan for more.",
{ status: 429 },
).send(res);
}

const { prompt, media } = req.body;

const textPrompt = `
Expand Down Expand Up @@ -71,8 +83,21 @@ export const generateCaptions = async (req: Request, res: Response) => {

await IncreaseCaptionGenerations(req.auth.id);
return new SuccessResponse("Captions generated successfully", { data: result }).send(res);
} catch (error) {
} catch (error: any) {
logger.error("Generate captions error", error);

// Handle Gemini API Quota Exceeded (429)
if (
error?.status === 429 ||
error?.message?.includes("429") ||
error?.message?.includes("quota")
) {
return new ErrorResponse(
"AI service limit reached. Please try again after some time or upgrade your plan.",
{ status: 429 },
).send(res);
}

return new ErrorResponse("Failed to generate captions").send(res);
}
};
Expand All @@ -83,6 +108,18 @@ export const generateCaptionsForSpecificPlatform = async (req: Request, res: Res
return new ErrorResponse("Unauthorized", { status: 401 }).send(res);
}

const user = await findUserByIdSafe(req.auth.id);
if (!user) {
return new ErrorResponse("User not found", { status: 404 }).send(res);
}

if (user.usage.captionGenerations >= user.limits.maxCaptionGenerations) {
return new ErrorResponse(
"You have reached your caption generation limit. Please upgrade your plan for more.",
{ status: 429 },
).send(res);
}

const { media, prompt } = req.body;
const platform = req.params.platform;

Expand All @@ -107,8 +144,21 @@ export const generateCaptionsForSpecificPlatform = async (req: Request, res: Res

await IncreaseCaptionGenerations(req.auth.id);
return new SuccessResponse("Captions are generated", { data: result }).send(res);
} catch (error) {
logger.error(error);
} catch (error: any) {
logger.error("Platform specific caption error", error);

// Handle Gemini API Quota Exceeded (429)
if (
error?.status === 429 ||
error?.message?.includes("429") ||
error?.message?.includes("quota")
) {
return new ErrorResponse(
"AI service limit reached. Please try again after some time or upgrade your plan.",
{ status: 429 },
).send(res);
}

return new ErrorResponse("cannot create caption").send(res);
}
};
14 changes: 13 additions & 1 deletion backend/src/controllers/post.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Types } from "mongoose";
import { z } from "zod";
import { getPresignedUploadUrl } from "../services/s3/s3.upload.service";
import { timezoneSchema, platformSpecificPostSchema } from "@hayon/schemas";
import { IncreasePostsCreated } from "../repositories/user.repository";
import { findUserByIdSafe, IncreasePostsCreated } from "../repositories/user.repository";
import { NotificationService } from "../services/notification.service";

const createPostSchema = z.object({
Expand Down Expand Up @@ -41,6 +41,18 @@ export const createPost = async (req: Request, res: Response) => {
return new ErrorResponse("Unauthorized", { status: 401 }).send(res);
}
const userId = req.auth.id;
const user = await findUserByIdSafe(userId);

if (!user) {
return new ErrorResponse("User not found", { status: 404 }).send(res);
}

if (user.usage.postsCreated >= user.limits.maxPosts) {
return new ErrorResponse(
"You have reached your post creation limit. Please upgrade your plan for more.",
{ status: 429 },
).send(res);
}

// Validation
const validationResult = createPostSchema.safeParse(req.body);
Expand Down
16 changes: 14 additions & 2 deletions backend/src/repositories/user.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,23 @@ export const changeUserName = async (userId: string, name: string) => {
};

export const IncreaseCaptionGenerations = async (userId: string) => {
return User.findByIdAndUpdate(userId, { $inc: { "usage.captionGenerations": 1 } }, { new: true });
const updated = await User.findByIdAndUpdate(
userId,
{ $inc: { "usage.captionGenerations": 1 } },
{ new: true },
);
await invalidateCache(`user:profile:${userId}`);
return updated;
};

export const IncreasePostsCreated = async (userId: string) => {
return User.findByIdAndUpdate(userId, { $inc: { "usage.postsCreated": 1 } }, { new: true });
const updated = await User.findByIdAndUpdate(
userId,
{ $inc: { "usage.postsCreated": 1 } },
{ new: true },
);
await invalidateCache(`user:profile:${userId}`);
return updated;
};

export const findAllUsers = async () => {
Expand Down
4 changes: 3 additions & 1 deletion backend/src/services/notification.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export class NotificationService {
link?: string,
) {
try {
// Strip HTML tags for the system pop-up
const plainTextMessage = message.replace(/<[^>]*>?/gm, "");
const user = await User.findById(recipientId).select("fcmTokens");

if (!user || !user.fcmTokens || user.fcmTokens.length === 0) {
Expand All @@ -72,7 +74,7 @@ export class NotificationService {
const messagePayload: any = {
notification: {
title,
body: message,
body: plainTextMessage,
},
tokens: user.fcmTokens,
};
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/app/(user)/dashboard/create-post/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { CreatePostForm } from "@/components/create-post/CreatePostForm";
import { PostPreview } from "@/components/create-post/PostPreview";
import { useSearchParams, useRouter } from "next/navigation";
import { SubmittingOverlay } from "@/components/create-post/SubmittingOverlay";
import { LimitExceededModal } from "@/components/shared/LimitExceededModal";

export default function CreatePostPage() {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
Expand Down Expand Up @@ -53,8 +54,11 @@ export default function CreatePostPage() {
platformPosts,
updatePlatformPost,
refinePlatformPostWithLLM,
platformGenerationErrors,
loadDraft,
draftId,
isLimitModalOpen,
setIsLimitModalOpen,
} = useCreatePost();

// Load draft if draftId query param is present
Expand Down Expand Up @@ -137,6 +141,8 @@ export default function CreatePostPage() {
availablePlatforms={availablePlatforms}
/>

<LimitExceededModal isOpen={isLimitModalOpen} onClose={() => setIsLimitModalOpen(false)} />

{/* Main Content Area */}
<main className="flex-1 px-4 py-6 lg:px-8 lg:py-8 overflow-y-auto custom-scrollbar relative flex flex-col">
{viewMode === "create" ? (
Expand All @@ -153,6 +159,7 @@ export default function CreatePostPage() {
platformWarnings={platformWarnings}
selectedPlatforms={selectedPlatforms}
availablePlatforms={availablePlatforms}
onOpenLimitModal={() => setIsLimitModalOpen(true)}
/>

{/* Right Column: Platform Selection */}
Expand Down Expand Up @@ -190,6 +197,7 @@ export default function CreatePostPage() {
platformPosts={platformPosts}
updatePlatformPost={updatePlatformPost}
refinePlatformPostWithLLM={refinePlatformPostWithLLM}
platformGenerationErrors={platformGenerationErrors}
mediaFiles={mediaFiles}
isGenerating={isGenerating}
platformWarnings={platformWarnings}
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/app/(user)/settings/notifications/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ export default function NotificationsPage() {
};

const highlightMessage = (message: string) => {
// If the message contains HTML tags, render it as HTML
if (message.includes("<") && message.includes(">")) {
return <span className="inline-block" dangerouslySetInnerHTML={{ __html: message }} />;
}

const platforms = ["bluesky", "threads", "tumblr", "mastodon", "facebook", "instagram"];
const statusKeywords = ["pending", "scheduled", "posted"];
const allKeywords = [...platforms, ...statusKeywords];
Expand Down
36 changes: 22 additions & 14 deletions frontend/src/components/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export default function LoginForm({
const error = searchParams.get("error");

const [email, setEmail] = useState("");

const [password, setPassword] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [formErrors, setFormErrors] = useState<FormErrors>({});
Expand All @@ -55,9 +56,9 @@ export default function LoginForm({
const message = errorMessages[error] || "An error occurred. Please try again.";

showToast("error", "Login failed", message);
window.history.replaceState({}, "", isAdmin ? "/admin-login" : "/admin/login");
window.history.replaceState({}, "", isAdmin ? "/admin/login" : "/login");
}
}, [error, isAdmin]);
}, [error, isAdmin, showToast]);

const validateForm = (): boolean => {
const result = loginSchema.safeParse({ email, password });
Expand All @@ -74,6 +75,11 @@ export default function LoginForm({
});

setFormErrors(errors);

// Show toast for the first validation error
if (zodErrors.errors.length > 0) {
showToast("error", "Invalid Input", zodErrors.errors[0].message);
}
return false;
}

Expand All @@ -91,30 +97,32 @@ export default function LoginForm({
setIsLoading(true);

try {
console.log("Login request");
const { data } = await api.post(loginEndpoint, { email, password });

// Store access token in memory
setAccessToken(data.data.accessToken);

async function setupPush(userId: string) {
console.log("Setting up push");
const permission = await Notification.requestPermission();
try {
const permission = await Notification.requestPermission();

if (permission !== "granted") {
console.log("Permission denied");
return;
}
if (permission !== "granted") {
return;
}

const token = await getToken(messaging, {
vapidKey: process.env.NEXT_PUBLIC_VAPID_KEY,
});
console.log("FCM Token:", token);
const token = await getToken(messaging, {
vapidKey: process.env.NEXT_PUBLIC_VAPID_KEY,
});

await api.post("/firebase/save-token", { userId, token });
await api.post("/firebase/save-token", { userId, token });
} catch (pushError) {
console.error("Push setup failed", pushError);
// Don't show toast for push failure to avoid distracting from login success
}
}

await setupPush(data.data.user.id);
showToast("success", "Welcome!", "You have logged in successfully.");
return router.push(redirectPath);
} catch (error) {
const axiosError = error as AxiosError<{ message?: string }>;
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/components/NotificationDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ export const NotificationDropdown = () => {
};

const highlightMessage = (message: string) => {
// If the message contains HTML tags, render it as HTML
if (message.includes("<") && message.includes(">")) {
return <span className="inline-block" dangerouslySetInnerHTML={{ __html: message }} />;
}

const platforms = ["bluesky", "threads", "tumblr", "mastodon", "facebook", "instagram"];
const statusKeywords = ["pending", "scheduled", "posted"];
const allKeywords = [...platforms, ...statusKeywords];
Expand Down
Loading