├── .gitignore ├── backend ├── .gitignore ├── README.md ├── bun.lockb ├── dist │ ├── bot │ │ ├── handlers │ │ │ ├── assign.handler.js │ │ │ ├── key.handler.js │ │ │ ├── message.handler.js │ │ │ ├── rules.handler.js │ │ │ └── start.handler.js │ │ └── index.js │ ├── emails │ │ └── subscription.email.js │ ├── index.js │ ├── lib │ │ ├── axios │ │ │ └── config.js │ │ ├── better-auth │ │ │ ├── auth-types.js │ │ │ ├── auth.js │ │ │ └── db.js │ │ ├── database │ │ │ ├── db.js │ │ │ └── model │ │ │ │ ├── customer.model.js │ │ │ │ ├── group.model.js │ │ │ │ ├── integration.model.js │ │ │ │ ├── payout.model.js │ │ │ │ ├── subscription.model.js │ │ │ │ ├── transaction.model.js │ │ │ │ └── wallet.model.js │ │ ├── env.js │ │ ├── paddle │ │ │ └── config.js │ │ ├── paypal │ │ │ └── utils.js │ │ ├── resend │ │ │ └── config.js │ │ └── telegram │ │ │ ├── config.js │ │ │ └── utils │ │ │ ├── constants.js │ │ │ └── index.js │ ├── middleware │ │ ├── cors.middleware.js │ │ ├── error.middleware.js │ │ ├── session.middleware.js │ │ └── unauthorized-access.middleware.js │ └── routes │ │ ├── crons │ │ ├── paypal.cron.js │ │ └── validator.cron.js │ │ ├── group.route.js │ │ ├── order.route.js │ │ ├── paddle-webhook.route.js │ │ ├── payout-history.route.js │ │ ├── paypal-payout.route.js │ │ ├── paypal-webhook.route.js │ │ ├── public-group.route.js │ │ ├── session.route.js │ │ ├── stats.route.js │ │ ├── v1.js │ │ └── wallet.route.js ├── env.example ├── package.json ├── src │ ├── bot │ │ ├── handlers │ │ │ ├── assign.handler.ts │ │ │ ├── key.handler.ts │ │ │ ├── message.handler.ts │ │ │ ├── rules.handler.ts │ │ │ └── start.handler.ts │ │ └── index.ts │ ├── emails │ │ └── subscription.email.tsx │ ├── index.ts │ ├── lib │ │ ├── axios │ │ │ └── config.ts │ │ ├── better-auth │ │ │ ├── auth-types.ts │ │ │ ├── auth.ts │ │ │ └── db.ts │ │ ├── database │ │ │ ├── db.ts │ │ │ └── model │ │ │ │ ├── customer.model.ts │ │ │ │ ├── group.model.ts │ │ │ │ ├── integration.model.ts │ │ │ │ ├── payout.model.ts │ │ │ │ ├── subscription.model.ts │ │ │ │ ├── transaction.model.ts │ │ │ │ └── wallet.model.ts │ │ ├── env.ts │ │ ├── paddle │ │ │ └── config.ts │ │ ├── paypal │ │ │ └── utils.ts │ │ ├── resend │ │ │ └── config.ts │ │ └── telegram │ │ │ ├── config.ts │ │ │ └── utils │ │ │ ├── constants.ts │ │ │ └── index.ts │ ├── middleware │ │ ├── cors.middleware.ts │ │ ├── error.middleware.ts │ │ ├── session.middleware.ts │ │ └── unauthorized-access.middleware.ts │ └── routes │ │ ├── crons │ │ ├── paypal.cron.ts │ │ └── validator.cron.ts │ │ ├── group.route.ts │ │ ├── order.route.ts │ │ ├── paddle-webhook.route.ts │ │ ├── payout-history.route.ts │ │ ├── paypal-payout.route.ts │ │ ├── paypal-webhook.route.ts │ │ ├── public-group.route.ts │ │ ├── session.route.ts │ │ ├── stats.route.ts │ │ ├── v1.ts │ │ └── wallet.route.ts └── tsconfig.json └── frontend ├── .gitignore ├── README.md ├── bun.lockb ├── components.json ├── env.examle ├── eslint.config.mjs ├── next.config.ts ├── package.json ├── postcss.config.mjs ├── public ├── paypal.png └── telegram.png ├── src ├── @types │ ├── models.ts │ └── response.ts ├── app │ ├── (auth) │ │ ├── _components │ │ │ └── auth-form.tsx │ │ ├── layout.tsx │ │ ├── sign-in │ │ │ └── page.tsx │ │ └── sign-up │ │ │ └── page.tsx │ ├── (checkout) │ │ ├── _components │ │ │ └── checkout.tsx │ │ └── checkout │ │ │ ├── [id] │ │ │ └── page.tsx │ │ │ └── loading.tsx │ ├── (dashboard) │ │ ├── _components │ │ │ ├── app-sidebar.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── header-profile.tsx │ │ │ ├── header.tsx │ │ │ └── page-header.tsx │ │ ├── dashboard │ │ │ ├── groups │ │ │ │ ├── _components │ │ │ │ │ ├── group-card.tsx │ │ │ │ │ └── groups-menu.tsx │ │ │ │ └── page.tsx │ │ │ ├── overview │ │ │ │ ├── _components │ │ │ │ │ ├── customers-table.tsx │ │ │ │ │ ├── overview-profile.tsx │ │ │ │ │ ├── overview-stats.tsx │ │ │ │ │ ├── overview.tsx │ │ │ │ │ └── transaction-table.tsx │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ └── payout │ │ │ │ ├── _components │ │ │ │ ├── balance-card.tsx │ │ │ │ ├── manage-payout.tsx │ │ │ │ ├── payout-history.tsx │ │ │ │ └── payout.tsx │ │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── (thank-you) │ │ ├── _components │ │ │ └── thak-you.tsx │ │ ├── layout.tsx │ │ └── thank-you │ │ │ └── [key] │ │ │ └── page.tsx │ ├── favicon.ico │ ├── globals.css │ └── layout.tsx ├── components │ ├── custom │ │ ├── button-styles.ts │ │ ├── p.tsx │ │ └── sidebar-trigger.tsx │ ├── icons │ │ └── kebab.tsx │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ └── tooltip.tsx ├── context │ ├── provider.tsx │ └── rtq-provider.tsx ├── hooks │ ├── use-mobile.tsx │ ├── use-paddle.tsx │ ├── use-toast.ts │ └── use-websocket.tsx ├── lib │ ├── axios │ │ └── config.ts │ ├── better-auth │ │ ├── auth-client.ts │ │ ├── auth-types.ts │ │ └── server-session.ts │ ├── env.ts │ ├── fetch │ │ └── group.fetch.ts │ ├── socket │ │ └── config.ts │ └── utils.ts └── middleware.ts ├── tailwind.config.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | /backend/cache/ 2 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # dev 2 | .yarn/ 3 | !.yarn/releases 4 | .vscode/* 5 | !.vscode/launch.json 6 | !.vscode/*.code-snippets 7 | .idea/workspace.xml 8 | .idea/usage.statistics.xml 9 | .idea/shelf 10 | 11 | # cache 12 | cache/ 13 | 14 | # deps 15 | node_modules/ 16 | 17 | # env 18 | .env 19 | .env.production 20 | .env.local 21 | 22 | # output 23 | dist/ 24 | 25 | # logs 26 | logs/ 27 | *.log 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | pnpm-debug.log* 32 | lerna-debug.log* 33 | 34 | # misc 35 | .DS_Store 36 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | npm install 3 | npm run dev 4 | ``` 5 | 6 | ``` 7 | open http://localhost:3000 8 | ``` 9 | -------------------------------------------------------------------------------- /backend/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devmdfaiz/group-gains/4d985225ebcebe7df13ac26a85bb3aa44067f7a7/backend/bun.lockb -------------------------------------------------------------------------------- /backend/dist/bot/handlers/key.handler.js: -------------------------------------------------------------------------------- 1 | import { Group } from "../../lib/database/model/group.model.js"; 2 | import { Subscription } from "../../lib/database/model/subscription.model.js"; 3 | import bot from "../../lib/telegram/config.js"; 4 | import { generateInviteLink } from "../../lib/telegram/utils/index.js"; 5 | export const keyVerify = async () => { 6 | bot.onText(/\/key (.+)/, async (msg, match) => { 7 | const userId = msg.from?.id; 8 | const chatId = msg.chat.id; // User's chat ID 9 | const key = match?.[1]; // Group ID provided by the user 10 | const subscription = await Subscription.findOne({ subscription_key: key }); 11 | if (!subscription) { 12 | bot.sendMessage(chatId, `May be your key is invalid or a server error occurred. Please try again`); 13 | return; 14 | } 15 | if (subscription.status !== "activated") { 16 | bot.sendMessage(chatId, `You subscription is not activated`); 17 | return; 18 | } 19 | const groupInfo = await Group.findOne({ _id: subscription.of_group }); 20 | if (!groupInfo) { 21 | bot.sendMessage(chatId, `Server error`); 22 | return; 23 | } 24 | if (userId) { 25 | await Subscription.updateOne({ telegram_user_id: userId }); 26 | } 27 | if (key && key === subscription.subscription_key) { 28 | // Generate Invite Link Here 29 | const link = (await generateInviteLink(Number(groupInfo.group_id), userId)) 30 | .invite_link; 31 | bot.sendMessage(chatId, `You key is valid. Invite Link: ${link}`); 32 | return; 33 | } 34 | bot.sendMessage(chatId, "Please provide a valid group ID."); 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /backend/dist/bot/handlers/message.handler.js: -------------------------------------------------------------------------------- 1 | import bot from "../../lib/telegram/config.js"; 2 | import { JOIN_GROUP, REGISTER_GROUP, } from "../../lib/telegram/utils/constants.js"; 3 | const joinResponse = `👋 **Welcome to the Group Registration!** 4 | 5 | To proceed, please enter your **subscription key** to join the group. 6 | 7 | 📝 **How to Enter Your Key:** 8 | If you have received your subscription key via email after completing the payment, type the following command: 9 | 10 | \`/key key_1234\` 11 | 12 | **Example:** 13 | \`/key key_1234\` 14 | 15 | ⚠️ **Note:** 16 | - Make sure to replace \`\`with the actual key you received. 17 | - If you haven’t received your key or need assistance, please contact us at [support@example.com](mailto:support@example.com). 18 | `; 19 | const registerResponse = `📢 To add your group, please make sure this bot has **admin permissions** in your group. 20 | Once done, type the following command in your group: 21 | 22 | \`/assign \` 23 | 24 | **Example:** \`/assign group_gain\` 25 | 26 | ⚠️ **Note:** Replace \`\` with your actual group gain username. If you face any issues, contact support at [support@example.com](mailto:support@example.com).`; 27 | export const sendMessage = () => { 28 | bot.on("message", async (msg) => { 29 | const chatId = msg.chat.id; 30 | const message = msg.text?.toLowerCase(); 31 | if (message === JOIN_GROUP.toLowerCase()) { 32 | bot.sendMessage(chatId, joinResponse, { parse_mode: "Markdown" }); 33 | return; 34 | } 35 | if (message === REGISTER_GROUP.toLowerCase()) { 36 | bot.sendMessage(chatId, registerResponse, { parse_mode: "Markdown" }); 37 | return; 38 | } 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /backend/dist/bot/handlers/rules.handler.js: -------------------------------------------------------------------------------- 1 | import bot from "../../lib/telegram/config.js"; 2 | export const rulesHandler = () => { 3 | bot.onText(/\/rules/, (msg) => { 4 | const chatId = msg.chat.id; 5 | const rules = ` 6 | 📜 Group Rules: 7 | 1. Be respectful. 8 | 2. No spamming. 9 | 3. Use appropriate language. 10 | 4. Follow admin instructions. 11 | `; 12 | bot.sendMessage(chatId, rules); 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /backend/dist/bot/handlers/start.handler.js: -------------------------------------------------------------------------------- 1 | import bot from "../../lib/telegram/config.js"; 2 | import { JOIN_GROUP, REGISTER_GROUP, } from "../../lib/telegram/utils/constants.js"; 3 | const replyKeyboard = { 4 | reply_markup: { 5 | keyboard: [ 6 | [{ text: REGISTER_GROUP }, { text: JOIN_GROUP }], // Row 1 7 | ], 8 | resize_keyboard: true, // Adjust size 9 | one_time_keyboard: true, // Hide after selection 10 | }, 11 | }; 12 | export const startHandler = () => { 13 | bot.onText(/\/start/, (msg) => { 14 | const chatId = msg.chat.id; 15 | bot.sendMessage(chatId, "Choose an option:", replyKeyboard); 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /backend/dist/bot/index.js: -------------------------------------------------------------------------------- 1 | import { assignGroup } from "./handlers/assign.handler.js"; 2 | import { keyVerify } from "./handlers/key.handler.js"; 3 | import { sendMessage } from "./handlers/message.handler.js"; 4 | import { rulesHandler } from "./handlers/rules.handler.js"; 5 | import { startHandler } from "./handlers/start.handler.js"; 6 | export const initBot = () => { 7 | // Register Handlers 8 | startHandler(); 9 | sendMessage(); 10 | rulesHandler(); 11 | assignGroup(); 12 | keyVerify(); 13 | }; 14 | -------------------------------------------------------------------------------- /backend/dist/emails/subscription.email.js: -------------------------------------------------------------------------------- 1 | import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime"; 2 | import { Html, Head, Body, Container, Section, Text, Link, Tailwind, Preview, Heading, Hr, Button, } from "@react-email/components"; 3 | const SubscriptionActivationEmail = ({ subscriberEmail = "subscriber@example.com", subscriptionKey = "1234-5678-91011", projectName = "Groups Gains", supportEmail = "support@groupsgains.com", }) => { 4 | const handleCopyKey = () => { 5 | navigator.clipboard.writeText(subscriptionKey); 6 | }; 7 | return (_jsxs(Html, { children: [_jsx(Head, { children: _jsx("title", { children: "Subscription Activation" }) }), _jsx(Preview, { children: `Activate your subscription with ${projectName}. Use the provided subscription key to complete the process.` }), _jsx(Tailwind, { children: _jsx(Body, { className: "bg-white my-auto mx-auto font-sans px-2", children: _jsxs(Container, { className: "border border-solid border-[#eaeaea] rounded my-[40px] mx-auto p-[20px] max-w-[465px]", children: [_jsxs(Heading, { className: "text-black text-[24px] font-normal text-center p-0 my-[30px] mx-0", children: ["Activate Your Subscription with ", _jsx("strong", { children: projectName })] }), _jsxs(Text, { className: "text-black text-[14px] leading-[24px]", children: ["Hello ", _jsx("strong", { children: subscriberEmail }), ","] }), _jsxs(Text, { className: "text-black text-[14px] leading-[24px]", children: ["Thank you for subscribing to ", _jsx("strong", { children: projectName }), ". Below is your subscription key to activate your account."] }), _jsxs(Section, { children: [_jsx(Text, { className: "text-black text-lg font-bold text-center", children: "Your Subscription Key:" }), _jsx(Text, { className: "text-black text-2xl font-bold px-4 py-1 text-center -mt-5", children: subscriptionKey }), _jsx(Button, { className: "bg-blue-600 text-white px-4 py-2 rounded-md block mx-auto mt-4", onClick: handleCopyKey, children: "Copy Subscription Key" })] }), _jsx(Section, { children: _jsxs(Text, { className: "text-black text-[14px] leading-[24px]", children: ["If you encounter any issues during activation, please contact our support team at", " ", _jsx(Link, { href: `mailto:${supportEmail}`, className: "text-blue-600 no-underline", children: supportEmail }), "."] }) }), _jsxs(Section, { children: [_jsxs(Text, { className: "text-black text-[14px] leading-[24px]", children: ["Thank you for choosing ", projectName, ". We look forward to serving you."] }), _jsx(Text, { children: "Best regards," }), _jsxs(Text, { children: [projectName, " Team"] })] }), _jsx(Hr, {}), _jsx(Section, { children: _jsx(Text, { className: "text-black text-[14px] leading-[24px]", children: "This email and its contents are confidential and intended solely for the individual or entity to whom it is addressed. If you have received this email in error, please notify the sender and delete the email from your system." }) })] }) }) })] })); 8 | }; 9 | export default SubscriptionActivationEmail; 10 | -------------------------------------------------------------------------------- /backend/dist/index.js: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | import { serve } from "@hono/node-server"; 3 | import { Hono } from "hono"; 4 | import { logger } from "hono/logger"; 5 | import { initBot } from "./bot/index.js"; 6 | import routes from "./routes/v1.js"; 7 | import errorHandler from "./middleware/error.middleware.js"; 8 | import db from "./lib/database/db.js"; 9 | import configCors from "./middleware/cors.middleware.js"; 10 | import { auth } from "./lib/better-auth/auth.js"; 11 | import addSession from "./middleware/session.middleware.js"; 12 | import sessionValidator from "./middleware/unauthorized-access.middleware.js"; 13 | import { Server as HttpServer } from "http"; 14 | import { Server } from "socket.io"; 15 | import { CLIENT_DOMAIN } from "./lib/env.js"; 16 | const app = new Hono(); 17 | const port = Number(process.env.PORT) || 8080; 18 | // Middleware stack 19 | app.use(logger()); 20 | app.use(addSession); 21 | app.use(configCors); 22 | app.use(sessionValidator); 23 | app.onError(errorHandler); 24 | // Database 25 | db(); 26 | // Auth Route 27 | app.on(["POST", "GET"], "/api/auth/**", (c) => { 28 | return auth.handler(c.req.raw); 29 | }); 30 | // Main Route 31 | app.get("/", (c) => c.text("Welcome to the Telegram Bot API!")); 32 | // Routes 33 | app.route("/api", routes); 34 | // Telegram Bot 35 | initBot(); 36 | const server = serve({ 37 | fetch: app.fetch, 38 | port, 39 | }); 40 | const io = new Server(server, { 41 | cors: { 42 | origin: CLIENT_DOMAIN, 43 | methods: ["GET", "POST", "OPTIONS"], 44 | credentials: true, 45 | }, 46 | }); 47 | // WebSocket Connection Setup 48 | io.on("connection", (socket) => { 49 | console.log("Client connected:", socket.id); 50 | socket.on("join", (key) => { 51 | socket.join(key); 52 | }); 53 | socket.on("disconnect", () => { 54 | console.log("Client disconnected:", socket.id); 55 | }); 56 | }); 57 | export { io }; 58 | -------------------------------------------------------------------------------- /backend/dist/lib/axios/config.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | export const paypalV2AxiosInstance = axios.create({ 3 | baseURL: "https://api-m.sandbox.paypal.com/v2", 4 | }); 5 | export const paypalV1AxiosInstance = axios.create({ 6 | baseURL: "https://api-m.sandbox.paypal.com/v1", 7 | }); 8 | -------------------------------------------------------------------------------- /backend/dist/lib/better-auth/auth-types.js: -------------------------------------------------------------------------------- 1 | import { auth } from "./auth.js"; 2 | export const Session = auth.$Infer.Session.session; 3 | export const User = auth.$Infer.Session.user; 4 | -------------------------------------------------------------------------------- /backend/dist/lib/better-auth/auth.js: -------------------------------------------------------------------------------- 1 | import { betterAuth } from "better-auth"; 2 | import { mongodbAdapter } from "better-auth/adapters/mongodb"; 3 | import client from "./db.js"; 4 | import { CLIENT_DOMAIN, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, } from "../env.js"; 5 | import { createAuthMiddleware } from "better-auth/api"; 6 | import db from "../database/db.js"; 7 | import { Wallet } from "../database/model/wallet.model.js"; 8 | import { ObjectId } from "mongodb"; 9 | const dbClient = client.db(); 10 | export const auth = betterAuth({ 11 | database: mongodbAdapter(dbClient), 12 | trustedOrigins: [CLIENT_DOMAIN], 13 | socialProviders: { 14 | github: { 15 | clientId: GITHUB_CLIENT_ID, 16 | clientSecret: GITHUB_CLIENT_SECRET, 17 | }, 18 | }, 19 | hooks: { 20 | after: createAuthMiddleware(async (c) => { 21 | const newSession = c.context.newSession; 22 | const user = newSession?.user; 23 | if (newSession && user) { 24 | try { 25 | await db(); 26 | const isWalletAvail = !!(await Wallet.findOne({ owner: user.id })); 27 | if (isWalletAvail) { 28 | return; 29 | } 30 | const wallet = await Wallet.create({ 31 | owner: user.id, 32 | }); 33 | const userCollection = dbClient.collection("user"); 34 | await userCollection.updateOne({ 35 | _id: new ObjectId(user.id), 36 | }, { 37 | $set: { wallet: wallet._id }, 38 | }); 39 | } 40 | catch (error) { 41 | console.log("Error in creating subscription in auth before hook: ", error); 42 | throw c.redirect("/sign-in"); 43 | } 44 | } 45 | }), 46 | }, 47 | appName: "Group Gains", 48 | advanced: { 49 | defaultCookieAttributes: { 50 | sameSite: "none", 51 | secure: true, 52 | }, 53 | }, 54 | }); 55 | -------------------------------------------------------------------------------- /backend/dist/lib/better-auth/db.js: -------------------------------------------------------------------------------- 1 | import { MongoClient, ServerApiVersion } from "mongodb"; 2 | import { MONGODB_URI } from "../env.js"; 3 | if (!MONGODB_URI) { 4 | throw new Error('Invalid/Missing environment variable: "MONGODB_URI"'); 5 | } 6 | const uri = MONGODB_URI; 7 | const options = { 8 | serverApi: { 9 | version: ServerApiVersion.v1, 10 | strict: true, 11 | deprecationErrors: true, 12 | }, 13 | }; 14 | let client; 15 | if (process.env.NODE_ENV === "development") { 16 | // In development mode, use a global variable so that the value 17 | // is preserved across module reloads caused by HMR (Hot Module Replacement). 18 | const globalWithMongo = global; 19 | if (!globalWithMongo._mongoClient) { 20 | globalWithMongo._mongoClient = new MongoClient(uri, options); 21 | } 22 | client = globalWithMongo._mongoClient; 23 | } 24 | else { 25 | // In production mode, it's best to not use a global variable. 26 | client = new MongoClient(uri, options); 27 | } 28 | // Export a module-scoped MongoClient. By doing this in a 29 | // separate module, the client can be shared across functions. 30 | export default client; 31 | -------------------------------------------------------------------------------- /backend/dist/lib/database/db.js: -------------------------------------------------------------------------------- 1 | import mongoose, {} from "mongoose"; 2 | import { MONGODB_URI } from "../env.js"; 3 | // Ensure the MongoDB URI is defined 4 | if (!MONGODB_URI) { 5 | throw new Error("Please define the MONGODB_URI environment variable inside .env"); 6 | } 7 | // Use a global cached object for maintaining a single connection instance 8 | const cached = global.mongoose || { 9 | conn: null, 10 | promise: null, 11 | }; 12 | if (!global.mongoose) { 13 | global.mongoose = cached; 14 | } 15 | async function db() { 16 | // Return the cached connection if available 17 | if (cached.conn) { 18 | console.log("Mongoose connection is already exists"); 19 | return cached.conn; 20 | } 21 | if (!cached.promise) { 22 | console.log("New mongoose connection is established"); 23 | const opts = { 24 | bufferCommands: false, 25 | }; 26 | // Create a new connection promise and cache it 27 | cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => { 28 | return mongoose; 29 | }); 30 | } 31 | try { 32 | cached.conn = await cached.promise; 33 | } 34 | catch (e) { 35 | cached.promise = null; // Reset the promise on failure 36 | throw e; // Rethrow the error to be handled by the caller 37 | } 38 | return cached.conn; 39 | } 40 | export default db; 41 | -------------------------------------------------------------------------------- /backend/dist/lib/database/model/customer.model.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema, Document, Model } from "mongoose"; 2 | const customerSchema = new Schema({ 3 | name: { type: String, required: true }, 4 | email: { type: String, required: true }, 5 | anonymous_key: { type: String, required: true, unique: true }, 6 | subscription: { 7 | type: mongoose.Schema.Types.ObjectId, 8 | ref: "Subscription", 9 | }, 10 | owner: { 11 | type: mongoose.Schema.Types.ObjectId, 12 | ref: "User", 13 | }, 14 | }, { timestamps: true }); 15 | export const Customer = mongoose.models.Customer || 16 | mongoose.model("Customer", customerSchema); 17 | -------------------------------------------------------------------------------- /backend/dist/lib/database/model/group.model.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema, Document, Model } from "mongoose"; 2 | const groupSchema = new Schema({ 3 | group_id: { type: Number, required: true, unique: true }, 4 | name: { type: String, required: true }, 5 | price_id: { type: String }, 6 | revenue: { type: Number, required: true, default: 0 }, 7 | currency_code: { type: String, default: "USD", required: true }, 8 | price: { type: Number, required: true, default: 0 }, 9 | owner: { 10 | type: mongoose.Schema.Types.ObjectId, 11 | ref: "User", 12 | required: true, 13 | }, 14 | }, { timestamps: true }); 15 | export const Group = mongoose.models.Group || mongoose.model("Group", groupSchema); 16 | -------------------------------------------------------------------------------- /backend/dist/lib/database/model/integration.model.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema, Document, Model } from "mongoose"; 2 | const integrationSchema = new Schema({ 3 | owner: { 4 | type: mongoose.Schema.Types.ObjectId, 5 | ref: "User", 6 | required: true, 7 | }, 8 | paypal: { 9 | email: { 10 | type: String, 11 | required: true, 12 | }, 13 | currency: { 14 | type: String, 15 | required: true, 16 | default: "USD" 17 | }, 18 | }, 19 | }, { timestamps: true }); 20 | export const Integration = mongoose.models.Integration || 21 | mongoose.model("Integration", integrationSchema); 22 | -------------------------------------------------------------------------------- /backend/dist/lib/database/model/payout.model.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema, Document, Model } from "mongoose"; 2 | const payoutSchema = new Schema({ 3 | owner: { 4 | type: mongoose.Schema.Types.ObjectId, 5 | ref: "User", 6 | required: true, 7 | }, 8 | amount: { 9 | type: Number, 10 | required: true, 11 | }, 12 | status: { 13 | type: String, 14 | required: true, 15 | default: "created", 16 | }, 17 | paypal: { 18 | payout_batch_id: { type: String, required: true }, 19 | payout_item_id: { type: String, required: true }, 20 | }, 21 | }, { timestamps: true }); 22 | export const Payout = mongoose.models.Payout || mongoose.model("Payout", payoutSchema); 23 | -------------------------------------------------------------------------------- /backend/dist/lib/database/model/subscription.model.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Model, Schema } from "mongoose"; 2 | import { Document } from "mongoose"; 3 | const subscriptionSchema = new Schema({ 4 | owner: { 5 | type: mongoose.Schema.Types.ObjectId, 6 | ref: "User", 7 | required: true, 8 | }, 9 | telegram_user_id: Number, 10 | subscriber: { 11 | type: mongoose.Schema.Types.ObjectId, 12 | ref: "Customer", 13 | }, 14 | currency_code: { type: String, default: "USD", required: true }, 15 | of_group: { 16 | type: mongoose.Schema.Types.ObjectId, 17 | ref: "Group", 18 | required: true, 19 | }, 20 | subscription_key: { type: String, required: true, unique: true }, 21 | anonymous_key: { type: String, required: true, unique: true }, 22 | amount: { 23 | type: Number, 24 | required: true, 25 | default: 0, 26 | }, 27 | billing: { 28 | cycle: { type: String, enum: ["month", "year"] }, 29 | billing_start: { type: Date }, 30 | billing_end: { type: Date }, 31 | }, 32 | status: { 33 | type: String, 34 | required: true, 35 | enum: ["activated", "canceled"], 36 | }, 37 | gateway: { 38 | provider: { 39 | type: String, 40 | enum: ["stripe", "paddle"], 41 | }, 42 | paddle: { 43 | price_id: { type: String }, 44 | subscription: { 45 | id: { type: String }, 46 | entity_type: { type: String }, 47 | }, 48 | }, 49 | }, 50 | }, { timestamps: true }); 51 | export const Subscription = mongoose.models.Subscription || 52 | mongoose.model("Subscription", subscriptionSchema); 53 | -------------------------------------------------------------------------------- /backend/dist/lib/database/model/transaction.model.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema, Document, Model } from "mongoose"; 2 | const transactionSchema = new Schema({ 3 | price: { 4 | type: Number, 5 | }, 6 | currency_code: { type: String, default: "USD", required: true }, 7 | status: { type: String, default: "created" }, 8 | subscription: { 9 | type: mongoose.Schema.Types.ObjectId, 10 | ref: "Subscription", 11 | }, 12 | of_group: { 13 | type: mongoose.Schema.Types.ObjectId, 14 | ref: "Group", 15 | required: true, 16 | }, 17 | anonymous_key: { type: String, required: true, unique: true }, 18 | owner: { 19 | type: mongoose.Schema.Types.ObjectId, 20 | ref: "User", 21 | }, 22 | }, { timestamps: true }); 23 | export const Transaction = mongoose.models.Transaction || 24 | mongoose.model("Transaction", transactionSchema); 25 | -------------------------------------------------------------------------------- /backend/dist/lib/database/model/wallet.model.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema, Document, Model } from "mongoose"; 2 | const walletSchema = new Schema({ 3 | owner: { 4 | type: mongoose.Schema.Types.ObjectId, 5 | ref: "User", 6 | required: true, 7 | }, 8 | balance: { type: Number, required: true, default: 0 }, 9 | withdraw: { type: Number, required: true, default: 0 }, 10 | }, { timestamps: true }); 11 | export const Wallet = mongoose.models.Wallet || mongoose.model("Wallet", walletSchema); 12 | -------------------------------------------------------------------------------- /backend/dist/lib/env.js: -------------------------------------------------------------------------------- 1 | export const MONGODB_URI = process.env.MONGODB_URI || ""; 2 | export const CLIENT_DOMAIN = process.env.CLIENT_DOMAIN || ""; 3 | export const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET || ""; 4 | export const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID || ""; 5 | // Paypal 6 | export const PAYPAL_CLIENT_ID = process.env.PAYPAL_CLIENT_ID || ""; 7 | export const PAYPAL_SECRET_KEY = process.env.PAYPAL_SECRET_KEY || ""; 8 | export const PAYPAL_WEBHOOK_ID = process.env.PAYPAL_WEBHOOK_ID || ""; 9 | // Paddle 10 | export const PADDLE_API_KEY = process.env.PADDLE_API_KEY || ""; 11 | export const PADDLE_PRODUCT_ID = process.env.PADDLE_PRODUCT_ID || ""; 12 | export const PADDLE_SUBSCRIPTION_WEBHOOK_SECRET_KEY = process.env.PADDLE_SUBSCRIPTION_WEBHOOK_SECRET_KEY || ""; 13 | -------------------------------------------------------------------------------- /backend/dist/lib/paddle/config.js: -------------------------------------------------------------------------------- 1 | import { Environment, LogLevel, Paddle } from '@paddle/paddle-node-sdk'; 2 | import { PADDLE_API_KEY } from '../env.js'; 3 | const paddle = new Paddle(PADDLE_API_KEY, { 4 | environment: Environment.sandbox, 5 | logLevel: LogLevel.verbose 6 | }); 7 | export default paddle; 8 | -------------------------------------------------------------------------------- /backend/dist/lib/paypal/utils.js: -------------------------------------------------------------------------------- 1 | import { paypalV1AxiosInstance } from "../axios/config.js"; 2 | import { PAYPAL_CLIENT_ID, PAYPAL_SECRET_KEY } from "../env.js"; 3 | export async function generatePaypalAccessToken() { 4 | const res = await paypalV1AxiosInstance.post("/oauth2/token", { grant_type: "client_credentials" }, { 5 | auth: { 6 | username: PAYPAL_CLIENT_ID, 7 | password: PAYPAL_SECRET_KEY, 8 | }, 9 | headers: { 10 | "Content-Type": "application/x-www-form-urlencoded", 11 | }, 12 | }); 13 | return res.data.access_token; 14 | } 15 | -------------------------------------------------------------------------------- /backend/dist/lib/resend/config.js: -------------------------------------------------------------------------------- 1 | import { Resend } from "resend"; 2 | const resend = new Resend(process.env.RESEND_API_KEY); 3 | export default resend; 4 | -------------------------------------------------------------------------------- /backend/dist/lib/telegram/config.js: -------------------------------------------------------------------------------- 1 | import TelegramBot from "node-telegram-bot-api"; 2 | const telegramToken = process.env.TELEGRAM_TOKEN || ""; 3 | const bot = new TelegramBot(telegramToken, { polling: true }); 4 | export default bot; 5 | -------------------------------------------------------------------------------- /backend/dist/lib/telegram/utils/constants.js: -------------------------------------------------------------------------------- 1 | export const REGISTER_GROUP = "Register Group"; 2 | export const JOIN_GROUP = "Join Group"; 3 | -------------------------------------------------------------------------------- /backend/dist/lib/telegram/utils/index.js: -------------------------------------------------------------------------------- 1 | import bot from "../config.js"; 2 | export const generateInviteLink = async (groupId, userId) => { 3 | await bot.unbanChatMember(groupId, userId, { only_if_banned: true }); 4 | const link = await bot.createChatInviteLink(groupId, { 5 | expire_date: Date.now() + 3600, 6 | member_limit: 1, 7 | }); 8 | return link; 9 | }; 10 | export async function removeUserFromGroup(chatId, userId) { 11 | if (!userId) { 12 | return; 13 | } 14 | await bot.banChatMember(chatId, userId); 15 | } 16 | -------------------------------------------------------------------------------- /backend/dist/middleware/cors.middleware.js: -------------------------------------------------------------------------------- 1 | import { cors } from "hono/cors"; 2 | import { CLIENT_DOMAIN } from "../lib/env.js"; 3 | const configCors = cors({ 4 | origin: CLIENT_DOMAIN, 5 | allowHeaders: ["Content-Type", "Authorization"], 6 | allowMethods: ["POST", "GET", "OPTIONS", "DELETE"], 7 | exposeHeaders: ["Content-Length"], 8 | maxAge: 600, 9 | credentials: true, 10 | }); 11 | export default configCors; 12 | -------------------------------------------------------------------------------- /backend/dist/middleware/error.middleware.js: -------------------------------------------------------------------------------- 1 | import { AxiosError } from "axios"; 2 | import { HTTPException } from "hono/http-exception"; 3 | const errorHandler = async (err, c) => { 4 | // Log the error details 5 | console.error("Caught error in error handler:", err); 6 | let response; 7 | // Check for specific error types 8 | if (err instanceof ValidationError) { 9 | // Custom validation error response 10 | response = { 11 | success: false, 12 | error: "Validation Error", 13 | message: err.message, 14 | result: err.details, // Add field-specific details if available 15 | }; 16 | return c.json(response, err.status); 17 | } 18 | if (err instanceof AxiosError) { 19 | response = { 20 | success: false, 21 | error: "External API Error", 22 | message: err.response?.data?.message || err.message, 23 | result: err.response?.data || null, 24 | }; 25 | const statusCode = err.status || 500; 26 | return c.json(response, { status: statusCode }); 27 | } 28 | if (err instanceof SyntaxError) { 29 | // Handle syntax errors (e.g., invalid JSON payloads) 30 | response = { 31 | success: false, 32 | error: "Bad Request", 33 | message: "Invalid JSON syntax in the request body.", 34 | result: null 35 | }; 36 | return c.json(response, { status: 400 }); 37 | } 38 | // Generic fallback error for unexpected issues 39 | response = { 40 | success: false, 41 | error: "Internal Server Error", 42 | message: "Something went wrong on our end. Please check result for more info.", 43 | result: err, 44 | }; 45 | return c.json(response, { status: 500 }); 46 | }; 47 | // Custom ValidationError class 48 | export class ValidationError extends HTTPException { 49 | details; 50 | message; 51 | constructor(message, details = {}, statusCode = 500) { 52 | const errorResponse = new Response(JSON.stringify({ 53 | error: "Validation Error", 54 | message, 55 | details, 56 | }), { 57 | status: statusCode, 58 | }); 59 | super(statusCode, { res: errorResponse }); 60 | this.details = details; 61 | this.message = message; 62 | } 63 | } 64 | export default errorHandler; 65 | -------------------------------------------------------------------------------- /backend/dist/middleware/session.middleware.js: -------------------------------------------------------------------------------- 1 | import { auth } from "../lib/better-auth/auth.js"; 2 | const addSession = async (c, next) => { 3 | const session = await auth.api.getSession({ headers: c.req.raw.headers }); 4 | if (!session) { 5 | c.set("user", null); 6 | c.set("session", null); 7 | return next(); 8 | } 9 | c.set("user", session.user); 10 | c.set("session", session.session); 11 | return next(); 12 | }; 13 | export default addSession; 14 | -------------------------------------------------------------------------------- /backend/dist/middleware/unauthorized-access.middleware.js: -------------------------------------------------------------------------------- 1 | import { ValidationError } from "./error.middleware.js"; 2 | const sessionValidator = (c, next) => { 3 | const user = c.get("user"); 4 | const path = c.req.path; 5 | if (path.startsWith("/api/v1/dashboard") && !user) { 6 | throw new ValidationError("Unauthorized access attempt detected.", { 7 | action: "access_protected_resource", 8 | requiredPermission: "user", 9 | receivedPermission: "unauthorized", 10 | }, 401); 11 | } 12 | return next(); 13 | }; 14 | export default sessionValidator; 15 | -------------------------------------------------------------------------------- /backend/dist/routes/crons/paypal.cron.js: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { Payout } from "../../lib/database/model/payout.model.js"; 3 | import { paypalV1AxiosInstance } from "../../lib/axios/config.js"; 4 | import { generatePaypalAccessToken } from "../../lib/paypal/utils.js"; 5 | import { io } from "../../index.js"; 6 | import { Wallet } from "../../lib/database/model/wallet.model.js"; 7 | const paypalCronRoute = new Hono(); 8 | paypalCronRoute.get("/paypal", async (c) => { 9 | const pendingPayouts = await Payout.find({ status: "PENDING" }); 10 | const token = await generatePaypalAccessToken(); 11 | for (const pendingPayout of pendingPayouts) { 12 | const payoutItemDetail = await paypalV1AxiosInstance.get(`/payments/payouts-item/${pendingPayout.paypal.payout_item_id}`, { 13 | headers: { 14 | Authorization: `Bearer ${token}`, 15 | }, 16 | }); 17 | const payoutItemStatus = payoutItemDetail.data.transaction_status; 18 | await Payout.updateOne({ _id: pendingPayout._id }, { status: payoutItemStatus }); 19 | if (payoutItemStatus === "SUCCESS") { 20 | await Wallet.updateOne({ owner: pendingPayout.owner }, { 21 | $inc: { 22 | balance: -pendingPayout.amount, 23 | withdraw: pendingPayout.amount, 24 | }, 25 | }); 26 | } 27 | io.to(pendingPayout.owner).emit("update-payout", "refetch"); 28 | io.to(pendingPayout.owner).emit("update-wallet", "refetch"); 29 | } 30 | return c.text("ok", 200); 31 | }); 32 | export default paypalCronRoute; 33 | -------------------------------------------------------------------------------- /backend/dist/routes/crons/validator.cron.js: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { Subscription } from "../../lib/database/model/subscription.model.js"; 3 | import { Group } from "../../lib/database/model/group.model.js"; 4 | import { removeUserFromGroup } from "../../lib/telegram/utils/index.js"; 5 | const validatorCron = new Hono(); 6 | validatorCron.get("/ban-user", async (c) => { 7 | const subscriptions = await Subscription.find({ status: "canceled" }); 8 | for (const subscription of subscriptions) { 9 | const group = await Group.findOne({ _id: subscription.of_group }); 10 | if (!group) { 11 | return c.text("error", 404); 12 | } 13 | await removeUserFromGroup(group.group_id, subscription.telegram_user_id); 14 | } 15 | return c.text("ok", 200); 16 | }); 17 | export default validatorCron; 18 | -------------------------------------------------------------------------------- /backend/dist/routes/group.route.js: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { Group } from "../lib/database/model/group.model.js"; 3 | import paddle from "../lib/paddle/config.js"; 4 | import { PADDLE_PRODUCT_ID } from "../lib/env.js"; 5 | const groupRoute = new Hono(); 6 | groupRoute.get("/groups", async (c) => { 7 | const user = c.get("user"); 8 | const groups = await Group.find({ owner: user.id }).sort({ createdAt: -1 }); 9 | return c.json({ 10 | success: true, 11 | message: "User groups have been successfully retrieved.", 12 | result: groups, 13 | }, 200); 14 | }); 15 | groupRoute.post("/groups", async (c) => { 16 | const user = c.get("user"); 17 | const { body } = await c.req.json(); 18 | const priceToUpdate = (Number(body.price) * 100).toString(); 19 | const groupInfo = await Group.findOne({ _id: body.id }); 20 | if (!groupInfo) { 21 | throw "Something went wrong. Please try again."; 22 | } 23 | let isPriceAvail; 24 | let updatedGroup; 25 | try { 26 | isPriceAvail = await paddle.prices.get(groupInfo.price_id); 27 | } 28 | catch (error) { 29 | console.log("Price is not exist. Let's create a new price here"); 30 | } 31 | if (isPriceAvail) { 32 | const price = await paddle.prices.update(groupInfo.price_id, { 33 | unitPrice: { 34 | amount: priceToUpdate, 35 | currencyCode: "USD", 36 | }, 37 | }); 38 | updatedGroup = await Group.findOneAndUpdate({ _id: body.id }, { 39 | price: body.price, 40 | price_id: price.id, 41 | }, { new: true }); 42 | } 43 | else { 44 | const price = await paddle.prices.create({ 45 | name: `Price for ${groupInfo?.name}`, 46 | productId: PADDLE_PRODUCT_ID, 47 | billingCycle: { 48 | interval: "month", 49 | frequency: 1, 50 | }, 51 | taxMode: "external", 52 | description: `Created by user ${user?.email}`, 53 | unitPrice: { 54 | amount: priceToUpdate, 55 | currencyCode: "USD", 56 | }, 57 | quantity: { 58 | minimum: 1, 59 | maximum: 9999999, 60 | }, 61 | }); 62 | updatedGroup = await Group.findOneAndUpdate({ _id: body.id }, { 63 | price: body.price, 64 | price_id: price.id, 65 | }, { new: true }); 66 | } 67 | return c.json({ 68 | success: true, 69 | message: "Group has been updated successfully", 70 | result: updatedGroup, 71 | }, 200); 72 | }); 73 | groupRoute.delete("/groups/:id", async (c) => { 74 | const id = c.req.param("id"); 75 | await Group.deleteOne({ _id: id }); 76 | return c.json({ 77 | success: true, 78 | message: "Group has been deleted successfully", 79 | result: id, 80 | }, 200); 81 | }); 82 | export default groupRoute; 83 | -------------------------------------------------------------------------------- /backend/dist/routes/order.route.js: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { Transaction } from "../lib/database/model/transaction.model.js"; 3 | import { Customer } from "../lib/database/model/customer.model.js"; 4 | const orderRoute = new Hono(); 5 | orderRoute.get("/order/:key", async (c) => { 6 | const key = c.req.param("key"); 7 | const transaction = await Transaction.findOne({ anonymous_key: key }); 8 | const customer = await Customer.findOne({ anonymous_key: key }); 9 | const result = { customer, transaction }; 10 | return c.json({ success: true, result }, 200); 11 | }); 12 | export default orderRoute; 13 | -------------------------------------------------------------------------------- /backend/dist/routes/payout-history.route.js: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { Payout } from "../lib/database/model/payout.model.js"; 3 | const payoutHistoryRoute = new Hono(); 4 | payoutHistoryRoute.get("/payout-history", async (c) => { 5 | const user = c.get("user"); 6 | const last7Days = new Date(); 7 | last7Days.setDate(last7Days.getDate() - 7); 8 | const payout = await Payout.find({ 9 | owner: user.id, 10 | createdAt: { $gte: last7Days }, 11 | }); 12 | return c.json({ success: true, message: "", result: payout }); 13 | }); 14 | export default payoutHistoryRoute; 15 | -------------------------------------------------------------------------------- /backend/dist/routes/public-group.route.js: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { Group } from "../lib/database/model/group.model.js"; 3 | const publicGroupRoute = new Hono(); 4 | publicGroupRoute.get("/groups/:id", async (c) => { 5 | const id = c.req.param("id"); 6 | const group = await Group.findOne({ _id: id }); 7 | return c.json({ 8 | success: true, 9 | message: "", 10 | result: group, 11 | }, 200); 12 | }); 13 | export default publicGroupRoute; 14 | -------------------------------------------------------------------------------- /backend/dist/routes/session.route.js: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | const sessionRoute = new Hono(); 3 | sessionRoute.get("/session", async (c) => { 4 | const session = c.get("session"); 5 | const user = c.get("user"); 6 | if (!user) 7 | return c.body(null, 401); 8 | return c.json({ 9 | session, 10 | user, 11 | }, 200); 12 | }); 13 | export default sessionRoute; 14 | -------------------------------------------------------------------------------- /backend/dist/routes/stats.route.js: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { Group } from "../lib/database/model/group.model.js"; 3 | import { Customer } from "../lib/database/model/customer.model.js"; 4 | import { Transaction } from "../lib/database/model/transaction.model.js"; 5 | const statsRoute = new Hono(); 6 | statsRoute.get("/overview", async (c) => { 7 | const user = c.get("user"); 8 | const last30Days = new Date(); 9 | const last7Days = new Date(); 10 | last30Days.setDate(last30Days.getDate() - 30); 11 | last7Days.setDate(last7Days.getDate() - 7); 12 | // 1. Calculate Total Revenue from Groups 13 | const groups = await Group.find({ 14 | owner: user.id, 15 | createdAt: { $gte: last30Days }, 16 | }); 17 | const earnings = groups.reduce((sum, group) => sum + group.revenue, 0); 18 | // 2. Count Transactions in the Last 30 Days 19 | const transactionCount = await Transaction.find({ 20 | owner: user.id, 21 | createdAt: { $gte: last7Days }, 22 | }).countDocuments(); 23 | const transactionDetails = await Transaction.find({ 24 | owner: user.id, 25 | createdAt: { $gte: last7Days }, 26 | }).sort({ createdAt: -1 }); 27 | // 3. Count Customers in the Last 30 Days 28 | const customerCount = await Customer.find({ 29 | owner: user.id, 30 | createdAt: { $gte: last7Days }, 31 | }).countDocuments(); 32 | const customerDetails = await Customer.find({ 33 | owner: user.id, 34 | createdAt: { $gte: last7Days }, 35 | }).sort({ createdAt: -1 }); 36 | // Format Results 37 | const result = { 38 | earnings: earnings || 0, 39 | totalCustomers: customerCount || 0, 40 | customerDetails: customerDetails || [], 41 | totalTransactions: transactionCount || 0, 42 | transactionDetails: transactionDetails || [], 43 | }; 44 | return c.json({ status: true, message: "", result }, 200); 45 | }); 46 | export default statsRoute; 47 | -------------------------------------------------------------------------------- /backend/dist/routes/v1.js: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import groupRoute from "./group.route.js"; 3 | import sessionRoute from "./session.route.js"; 4 | import publicGroupRoute from "./public-group.route.js"; 5 | import paddleWebhookRoute from "./paddle-webhook.route.js"; 6 | import statsRoute from "./stats.route.js"; 7 | import orderRoute from "./order.route.js"; 8 | import paypalRoute from "./paypal-payout.route.js"; 9 | import walletRoute from "./wallet.route.js"; 10 | import payoutHistoryRoute from "./payout-history.route.js"; 11 | import paypalWebhook from "./paypal-webhook.route.js"; 12 | import validatorCron from "./crons/validator.cron.js"; 13 | import paypalCronRoute from "./crons/paypal.cron.js"; 14 | const routes = new Hono(); 15 | // Protected Route - Dashboard 16 | routes.route("/v1/dashboard", groupRoute); 17 | routes.route("/v1/dashboard", statsRoute); 18 | routes.route("/v1/dashboard", walletRoute); 19 | routes.route("/v1/dashboard", payoutHistoryRoute); 20 | // Protected Route - Payment 21 | routes.route("/v1/dashboard/paypal", paypalRoute); 22 | // Public Route - Groups for order 23 | routes.route("/v1", publicGroupRoute); 24 | routes.route("/v1", orderRoute); // For Thankyou page 25 | // Public Route - Paddle webhook 26 | routes.route("/v1/webhook", paddleWebhookRoute); 27 | routes.route("/v1/webhook", paypalWebhook); 28 | // Public Route - To get session on client side 29 | routes.route("/v1/user", sessionRoute); 30 | // Crons 31 | routes.route("/v1/cron", validatorCron); // To ban user 32 | routes.route("/v1/cron", paypalCronRoute); 33 | export default routes; 34 | -------------------------------------------------------------------------------- /backend/dist/routes/wallet.route.js: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { Wallet } from "../lib/database/model/wallet.model.js"; 3 | const walletRoute = new Hono(); 4 | walletRoute.get("/wallet", async (c) => { 5 | const user = c.get("user"); 6 | const wallet = await Wallet.findOne({ owner: user.id }); 7 | return c.json({ success: true, message: "", result: wallet }, 200); 8 | }); 9 | export default walletRoute; 10 | -------------------------------------------------------------------------------- /backend/env.example: -------------------------------------------------------------------------------- 1 | # General 2 | PORT=8080 3 | CLIENT_DOMAIN=https://localhost:3000 4 | # CLIENT_DOMAIN=* 5 | 6 | # Database 7 | MONGODB_URI=mongodb://localhost:27017/group-gains 8 | 9 | # Better Auth 10 | BETTER_AUTH_URL=http://localhost:8080 11 | BETTER_AUTH_SECRET=br8dl1Wk9qCpPDAhdr0tiLS99FfQ4ukE 12 | 13 | # OAuth 14 | GOOGLE_CLIENT_ID== 15 | GOOGLE_CLIENT_SECRET= 16 | 17 | # Telegram 18 | TELEGRAM_TOKEN= 19 | 20 | # Paypal 21 | PAYPAL_WEBHOOK_ID= 22 | 23 | PAYPAL_CLIENT_ID= 24 | 25 | PAYPAL_SECRET_KEY= 26 | 27 | # Paddle 28 | PADDLE_API_KEY= 29 | PADDLE_PRODUCT_ID= 30 | PADDLE_SUBSCRIPTION_WEBHOOK_SECRET_KEY= 31 | 32 | # Resend 33 | RESEND_API_KEY= -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "group-gains-server", 3 | "type": "module", 4 | "scripts": { 5 | "dev": "tsx watch src/index.ts", 6 | "build": "tsc", 7 | "start": "node dist/index.js", 8 | "email": "email dev --dir src/emails" 9 | }, 10 | "dependencies": { 11 | "@hono/node-server": "^1.13.7", 12 | "@paddle/paddle-node-sdk": "^2.5.0", 13 | "@react-email/components": "0.0.32", 14 | "@react-email/render": "1.0.4", 15 | "axios": "^1.7.9", 16 | "better-auth": "^1.1.14", 17 | "buffer-crc32": "^1.0.0", 18 | "crc-32": "^1.2.2", 19 | "dotenv": "^16.4.7", 20 | "hono": "^4.6.17", 21 | "mongodb": "^6.12.0", 22 | "mongoose": "^8.9.5", 23 | "node-telegram-bot-api": "^0.66.0", 24 | "react": "19.0.0", 25 | "react-dom": "19.0.0", 26 | "resend": "^4.1.1", 27 | "socket.io": "^4.8.1" 28 | }, 29 | "devDependencies": { 30 | "@types/buffer-crc32": "^0.2.4", 31 | "@types/node": "^20.11.17", 32 | "@types/node-telegram-bot-api": "^0.64.7", 33 | "react-email": "3.0.6", 34 | "tsx": "^4.7.1", 35 | "typescript": "^5.7.3" 36 | } 37 | } -------------------------------------------------------------------------------- /backend/src/bot/handlers/key.handler.ts: -------------------------------------------------------------------------------- 1 | import { Group } from "../../lib/database/model/group.model.js"; 2 | import { Subscription } from "../../lib/database/model/subscription.model.js"; 3 | import bot from "../../lib/telegram/config.js"; 4 | import { generateInviteLink } from "../../lib/telegram/utils/index.js"; 5 | 6 | export const keyVerify = async () => { 7 | bot.onText(/\/key (.+)/, async (msg, match) => { 8 | const userId = msg.from?.id; 9 | const chatId = msg.chat.id; // User's chat ID 10 | const key = match?.[1]; // Group ID provided by the user 11 | 12 | const subscription = await Subscription.findOne({ subscription_key: key }); 13 | 14 | if (!subscription) { 15 | bot.sendMessage( 16 | chatId, 17 | `May be your key is invalid or a server error occurred. Please try again` 18 | ); 19 | return; 20 | } 21 | 22 | if (subscription.status !== "activated") { 23 | bot.sendMessage(chatId, `You subscription is not activated`); 24 | return; 25 | } 26 | 27 | const groupInfo = await Group.findOne({ _id: subscription.of_group }); 28 | 29 | if (!groupInfo) { 30 | bot.sendMessage(chatId, `Server error`); 31 | return; 32 | } 33 | 34 | if (userId) { 35 | await Subscription.updateOne({ telegram_user_id: userId }); 36 | } 37 | 38 | if (key && key === subscription.subscription_key) { 39 | // Generate Invite Link Here 40 | const link = (await generateInviteLink(Number(groupInfo.group_id), userId as number)) 41 | .invite_link; 42 | 43 | bot.sendMessage(chatId, `You key is valid. Invite Link: ${link}`); 44 | 45 | return; 46 | } 47 | 48 | bot.sendMessage(chatId, "Please provide a valid group ID."); 49 | }); 50 | }; 51 | -------------------------------------------------------------------------------- /backend/src/bot/handlers/message.handler.ts: -------------------------------------------------------------------------------- 1 | import bot from "../../lib/telegram/config.js"; 2 | import { 3 | JOIN_GROUP, 4 | REGISTER_GROUP, 5 | } from "../../lib/telegram/utils/constants.js"; 6 | 7 | const joinResponse = `👋 **Welcome to the Group Registration!** 8 | 9 | To proceed, please enter your **subscription key** to join the group. 10 | 11 | 📝 **How to Enter Your Key:** 12 | If you have received your subscription key via email after completing the payment, type the following command: 13 | 14 | \`/key key_1234\` 15 | 16 | **Example:** 17 | \`/key key_1234\` 18 | 19 | ⚠️ **Note:** 20 | - Make sure to replace \`\`with the actual key you received. 21 | - If you haven’t received your key or need assistance, please contact us at [support@example.com](mailto:support@example.com). 22 | `; 23 | 24 | const registerResponse = `📢 To add your group, please make sure this bot has **admin permissions** in your group. 25 | Once done, type the following command in your group: 26 | 27 | \`/assign \` 28 | 29 | **Example:** \`/assign group_gain\` 30 | 31 | ⚠️ **Note:** Replace \`\` with your actual group gain username. If you face any issues, contact support at [support@example.com](mailto:support@example.com).`; 32 | 33 | export const sendMessage = () => { 34 | bot.on("message", async (msg) => { 35 | const chatId = msg.chat.id; 36 | const message = msg.text?.toLowerCase(); 37 | 38 | if (message === JOIN_GROUP.toLowerCase()) { 39 | bot.sendMessage(chatId, joinResponse, { parse_mode: "Markdown" }); 40 | return; 41 | } 42 | 43 | if (message === REGISTER_GROUP.toLowerCase()) { 44 | bot.sendMessage(chatId, registerResponse, { parse_mode: "Markdown" }); 45 | return; 46 | } 47 | }); 48 | }; 49 | -------------------------------------------------------------------------------- /backend/src/bot/handlers/rules.handler.ts: -------------------------------------------------------------------------------- 1 | import bot from "../../lib/telegram/config.js"; 2 | 3 | export const rulesHandler = () => { 4 | bot.onText(/\/rules/, (msg) => { 5 | const chatId = msg.chat.id; 6 | 7 | const rules = ` 8 | 📜 Group Rules: 9 | 1. Be respectful. 10 | 2. No spamming. 11 | 3. Use appropriate language. 12 | 4. Follow admin instructions. 13 | `; 14 | 15 | bot.sendMessage(chatId, rules); 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /backend/src/bot/handlers/start.handler.ts: -------------------------------------------------------------------------------- 1 | import type { SendMessageOptions } from "node-telegram-bot-api"; 2 | import bot from "../../lib/telegram/config.js"; 3 | import { 4 | JOIN_GROUP, 5 | REGISTER_GROUP, 6 | } from "../../lib/telegram/utils/constants.js"; 7 | 8 | const replyKeyboard = { 9 | reply_markup: { 10 | keyboard: [ 11 | [{ text: REGISTER_GROUP }, { text: JOIN_GROUP }], // Row 1 12 | ], 13 | resize_keyboard: true, // Adjust size 14 | one_time_keyboard: true, // Hide after selection 15 | }, 16 | } as SendMessageOptions; 17 | 18 | export const startHandler = () => { 19 | bot.onText(/\/start/, (msg) => { 20 | const chatId = msg.chat.id; 21 | 22 | bot.sendMessage(chatId, "Choose an option:", replyKeyboard); 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /backend/src/bot/index.ts: -------------------------------------------------------------------------------- 1 | import { assignGroup } from "./handlers/assign.handler.js"; 2 | import { keyVerify } from "./handlers/key.handler.js"; 3 | import { sendMessage } from "./handlers/message.handler.js"; 4 | import { rulesHandler } from "./handlers/rules.handler.js"; 5 | import { startHandler } from "./handlers/start.handler.js"; 6 | 7 | export const initBot = () => { 8 | // Register Handlers 9 | startHandler(); 10 | sendMessage() 11 | rulesHandler() 12 | assignGroup() 13 | keyVerify() 14 | }; 15 | -------------------------------------------------------------------------------- /backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | import { serve } from "@hono/node-server"; 3 | import { Hono } from "hono"; 4 | import { logger } from "hono/logger"; 5 | import { initBot } from "./bot/index.js"; 6 | import routes from "./routes/v1.js"; 7 | import errorHandler from "./middleware/error.middleware.js"; 8 | import db from "./lib/database/db.js"; 9 | import configCors from "./middleware/cors.middleware.js"; 10 | import { auth } from "./lib/better-auth/auth.js"; 11 | import addSession from "./middleware/session.middleware.js"; 12 | import sessionValidator from "./middleware/unauthorized-access.middleware.js"; 13 | import { Server as HttpServer } from "http"; 14 | import { Server } from "socket.io"; 15 | import { CLIENT_DOMAIN } from "./lib/env.js"; 16 | 17 | const app = new Hono(); 18 | const port = Number(process.env.PORT) || 8080; 19 | 20 | // Middleware stack 21 | app.use(logger()); 22 | app.use(addSession); 23 | app.use(configCors); 24 | app.use(sessionValidator); 25 | 26 | app.onError(errorHandler); 27 | 28 | // Database 29 | db(); 30 | 31 | // Auth Route 32 | app.on(["POST", "GET"], "/api/auth/**", (c) => { 33 | return auth.handler(c.req.raw); 34 | }); 35 | 36 | // Main Route 37 | app.get("/", (c) => c.text("Welcome to the Telegram Bot API!")); 38 | 39 | // Routes 40 | app.route("/api", routes); 41 | 42 | // Telegram Bot 43 | initBot(); 44 | 45 | const server = serve({ 46 | fetch: app.fetch, 47 | port, 48 | }); 49 | 50 | const io = new Server(server as HttpServer, { 51 | cors: { 52 | origin: CLIENT_DOMAIN, 53 | methods: ["GET", "POST", "OPTIONS"], 54 | credentials: true, 55 | }, 56 | }); 57 | 58 | // WebSocket Connection Setup 59 | io.on("connection", (socket) => { 60 | console.log("Client connected:", socket.id); 61 | 62 | socket.on("join", (key) => { 63 | socket.join(key); 64 | }); 65 | 66 | socket.on("disconnect", () => { 67 | console.log("Client disconnected:", socket.id); 68 | }); 69 | }); 70 | 71 | export { io }; 72 | -------------------------------------------------------------------------------- /backend/src/lib/axios/config.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const paypalV2AxiosInstance = axios.create({ 4 | baseURL: "https://api-m.sandbox.paypal.com/v2", 5 | }); 6 | 7 | export const paypalV1AxiosInstance = axios.create({ 8 | baseURL: "https://api-m.sandbox.paypal.com/v1", 9 | }); 10 | -------------------------------------------------------------------------------- /backend/src/lib/better-auth/auth-types.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "./auth.js"; 2 | 3 | export const Session = auth.$Infer.Session.session; 4 | export const User = auth.$Infer.Session.user; 5 | 6 | export interface AuthSession { 7 | Variables: { 8 | user: typeof User | null; 9 | session: typeof Session | null; 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/lib/better-auth/auth.ts: -------------------------------------------------------------------------------- 1 | import { betterAuth } from "better-auth"; 2 | import { mongodbAdapter } from "better-auth/adapters/mongodb"; 3 | import client from "./db.js"; 4 | import { 5 | CLIENT_DOMAIN, 6 | GITHUB_CLIENT_ID, 7 | GITHUB_CLIENT_SECRET, 8 | } from "../env.js"; 9 | import { createAuthMiddleware } from "better-auth/api"; 10 | import db from "../database/db.js"; 11 | import { Wallet } from "../database/model/wallet.model.js"; 12 | import { ObjectId } from "mongodb"; 13 | 14 | const dbClient = client.db(); 15 | 16 | export const auth = betterAuth({ 17 | database: mongodbAdapter(dbClient), 18 | trustedOrigins: [CLIENT_DOMAIN], 19 | socialProviders: { 20 | github: { 21 | clientId: GITHUB_CLIENT_ID, 22 | clientSecret: GITHUB_CLIENT_SECRET, 23 | }, 24 | }, 25 | hooks: { 26 | after: createAuthMiddleware(async (c) => { 27 | const newSession = c.context.newSession; 28 | const user = newSession?.user; 29 | 30 | if (newSession && user) { 31 | try { 32 | await db(); 33 | 34 | const isWalletAvail = !!(await Wallet.findOne({ owner: user.id })); 35 | 36 | if (isWalletAvail) { 37 | return; 38 | } 39 | 40 | const wallet = await Wallet.create({ 41 | owner: user.id, 42 | }); 43 | 44 | const userCollection = dbClient.collection("user"); 45 | 46 | await userCollection.updateOne( 47 | { 48 | _id: new ObjectId(user.id), 49 | }, 50 | { 51 | $set: { wallet: wallet._id }, 52 | } 53 | ); 54 | } catch (error) { 55 | console.log( 56 | "Error in creating subscription in auth before hook: ", 57 | error 58 | ); 59 | 60 | throw c.redirect("/sign-in"); 61 | } 62 | } 63 | }), 64 | }, 65 | appName: "Group Gains", 66 | advanced: { 67 | defaultCookieAttributes: { 68 | sameSite: "none", 69 | secure: true, 70 | }, 71 | }, 72 | }); 73 | -------------------------------------------------------------------------------- /backend/src/lib/better-auth/db.ts: -------------------------------------------------------------------------------- 1 | import { MongoClient, ServerApiVersion } from "mongodb" 2 | import { MONGODB_URI } from "../env.js" 3 | 4 | if (!MONGODB_URI) { 5 | throw new Error('Invalid/Missing environment variable: "MONGODB_URI"') 6 | } 7 | 8 | const uri = MONGODB_URI 9 | const options = { 10 | serverApi: { 11 | version: ServerApiVersion.v1, 12 | strict: true, 13 | deprecationErrors: true, 14 | }, 15 | } 16 | 17 | let client: MongoClient 18 | 19 | if (process.env.NODE_ENV === "development") { 20 | // In development mode, use a global variable so that the value 21 | // is preserved across module reloads caused by HMR (Hot Module Replacement). 22 | const globalWithMongo = global as typeof globalThis & { 23 | _mongoClient?: MongoClient 24 | } 25 | 26 | if (!globalWithMongo._mongoClient) { 27 | globalWithMongo._mongoClient = new MongoClient(uri, options) 28 | } 29 | client = globalWithMongo._mongoClient 30 | } else { 31 | // In production mode, it's best to not use a global variable. 32 | client = new MongoClient(uri, options) 33 | } 34 | 35 | // Export a module-scoped MongoClient. By doing this in a 36 | // separate module, the client can be shared across functions. 37 | export default client -------------------------------------------------------------------------------- /backend/src/lib/database/db.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { type ConnectOptions } from "mongoose"; 2 | import { MONGODB_URI } from "../env.js"; 3 | 4 | // Ensure the MongoDB URI is defined 5 | if (!MONGODB_URI) { 6 | throw new Error( 7 | "Please define the MONGODB_URI environment variable inside .env" 8 | ); 9 | } 10 | 11 | // Define a type for the cached object 12 | interface MongooseCache { 13 | conn: mongoose.Mongoose | null; 14 | promise: Promise | null; 15 | } 16 | 17 | // Add the cache object to the global type to prevent TypeScript errors 18 | declare global { 19 | // eslint-disable-next-line no-var 20 | var mongoose: MongooseCache | undefined; 21 | } 22 | 23 | // Use a global cached object for maintaining a single connection instance 24 | const cached: MongooseCache = global.mongoose || { 25 | conn: null, 26 | promise: null, 27 | }; 28 | 29 | if (!global.mongoose) { 30 | global.mongoose = cached; 31 | } 32 | 33 | async function db(): Promise { 34 | // Return the cached connection if available 35 | if (cached.conn) { 36 | console.log("Mongoose connection is already exists"); 37 | return cached.conn; 38 | } 39 | 40 | if (!cached.promise) { 41 | console.log("New mongoose connection is established"); 42 | const opts: ConnectOptions = { 43 | bufferCommands: false, 44 | }; 45 | 46 | // Create a new connection promise and cache it 47 | cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => { 48 | return mongoose; 49 | }); 50 | } 51 | 52 | try { 53 | cached.conn = await cached.promise; 54 | } catch (e) { 55 | cached.promise = null; // Reset the promise on failure 56 | throw e; // Rethrow the error to be handled by the caller 57 | } 58 | 59 | return cached.conn; 60 | } 61 | 62 | export default db; -------------------------------------------------------------------------------- /backend/src/lib/database/model/customer.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema, Document, Model } from "mongoose"; 2 | 3 | export interface ICustomer { 4 | _id: string; 5 | name: string; 6 | anonymous_key: string; 7 | email: string; 8 | subscription: string | mongoose.Schema.Types.ObjectId; 9 | owner: string | mongoose.Schema.Types.ObjectId; 10 | createdAt: Date; 11 | updatedAt: Date; 12 | } 13 | 14 | // Extend Mongoose's Document and override _id type 15 | interface CustomerModel extends Omit, Document { 16 | _id: string; 17 | } 18 | 19 | const customerSchema: Schema = new Schema( 20 | { 21 | name: { type: String, required: true }, 22 | email: { type: String, required: true }, 23 | anonymous_key: { type: String, required: true, unique: true }, 24 | subscription: { 25 | type: mongoose.Schema.Types.ObjectId, 26 | ref: "Subscription", 27 | }, 28 | owner: { 29 | type: mongoose.Schema.Types.ObjectId, 30 | ref: "User", 31 | }, 32 | }, 33 | { timestamps: true } 34 | ); 35 | 36 | export const Customer: Model = 37 | mongoose.models.Customer || 38 | mongoose.model("Customer", customerSchema); 39 | -------------------------------------------------------------------------------- /backend/src/lib/database/model/group.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema, Document, Model } from "mongoose"; 2 | 3 | export interface IGroup { 4 | _id: string; 5 | group_id: number; // Use snake_case here 6 | name: string; 7 | currency_code: string; 8 | price_id: string; 9 | revenue: number; 10 | price: number; 11 | owner: string | mongoose.Schema.Types.ObjectId; 12 | createdAt: Date; 13 | updatedAt: Date; 14 | } 15 | 16 | // Extend Mongoose's Document and override _id type 17 | interface GroupModel extends Omit, Document { 18 | _id: string; 19 | } 20 | 21 | const groupSchema: Schema = new Schema( 22 | { 23 | group_id: { type: Number, required: true, unique: true }, 24 | name: { type: String, required: true }, 25 | price_id: { type: String }, 26 | revenue: { type: Number, required: true, default: 0 }, 27 | currency_code: { type: String, default: "USD", required: true }, 28 | price: { type: Number, required: true, default: 0 }, 29 | owner: { 30 | type: mongoose.Schema.Types.ObjectId, 31 | ref: "User", 32 | required: true, 33 | }, 34 | }, 35 | { timestamps: true } 36 | ); 37 | 38 | export const Group: Model = 39 | mongoose.models.Group || mongoose.model("Group", groupSchema); 40 | -------------------------------------------------------------------------------- /backend/src/lib/database/model/integration.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema, Document, Model } from "mongoose"; 2 | 3 | export interface IIntegration { 4 | _id: string; 5 | owner: string | mongoose.Schema.Types.ObjectId; 6 | paypal: { 7 | email: string; 8 | currency: string; 9 | }; 10 | createdAt: Date; 11 | updatedAt: Date; 12 | } 13 | 14 | // Extend Mongoose's Document and override _id type 15 | interface IntegrationModel extends Omit, Document { 16 | _id: string; 17 | } 18 | 19 | const integrationSchema: Schema = new Schema( 20 | { 21 | owner: { 22 | type: mongoose.Schema.Types.ObjectId, 23 | ref: "User", 24 | required: true, 25 | }, 26 | paypal: { 27 | email: { 28 | type: String, 29 | required: true, 30 | }, 31 | currency: { 32 | type: String, 33 | required: true, 34 | default: "USD" 35 | }, 36 | }, 37 | }, 38 | { timestamps: true } 39 | ); 40 | 41 | export const Integration: Model = 42 | mongoose.models.Integration || 43 | mongoose.model("Integration", integrationSchema); 44 | -------------------------------------------------------------------------------- /backend/src/lib/database/model/payout.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema, Document, Model } from "mongoose"; 2 | 3 | export interface IPayout { 4 | _id: string; 5 | owner: string | mongoose.Schema.Types.ObjectId; 6 | amount: number; 7 | paypal: { 8 | payout_batch_id: string; 9 | payout_item_id: string; 10 | }; 11 | status: string; 12 | createdAt: Date; 13 | updatedAt: Date; 14 | } 15 | 16 | // Extend Mongoose's Document and override _id type 17 | interface PayoutModel extends Omit, Document { 18 | _id: string; 19 | } 20 | 21 | const payoutSchema: Schema = new Schema( 22 | { 23 | owner: { 24 | type: mongoose.Schema.Types.ObjectId, 25 | ref: "User", 26 | required: true, 27 | }, 28 | amount: { 29 | type: Number, 30 | required: true, 31 | }, 32 | status: { 33 | type: String, 34 | required: true, 35 | default: "created", 36 | }, 37 | paypal: { 38 | payout_batch_id: { type: String, required: true }, 39 | payout_item_id: { type: String, required: true }, 40 | }, 41 | }, 42 | { timestamps: true } 43 | ); 44 | 45 | export const Payout: Model = 46 | mongoose.models.Payout || mongoose.model("Payout", payoutSchema); 47 | -------------------------------------------------------------------------------- /backend/src/lib/database/model/subscription.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Model, Schema } from "mongoose"; 2 | import { Document } from "mongoose"; 3 | 4 | export interface ISubscription { 5 | _id: mongoose.Types.ObjectId | string; 6 | owner: mongoose.Types.ObjectId | string; 7 | subscriber: mongoose.Types.ObjectId | string; 8 | of_group: mongoose.Types.ObjectId | string; 9 | subscription_key: string; 10 | price: number; 11 | currency_code: string; 12 | amount: number; 13 | telegram_user_id: number; 14 | billing: { 15 | cycle: "month" | "year"; 16 | billing_start: Date; 17 | billing_end: Date; 18 | }; 19 | status: "activated" | "canceled"; 20 | anonymous_key: string; 21 | gateway?: { 22 | provider: "stripe" | "paddle"; 23 | paddle: { 24 | price_id: string; 25 | subscription: { 26 | id: string; 27 | entity_type: string; 28 | }; 29 | }; 30 | }; 31 | createdAt: Date; 32 | updatedAt: Date; 33 | } 34 | 35 | // Extend Mongoose's Document and override _id type 36 | interface SubscriptionModel extends Omit, Document { 37 | _id: string; 38 | } 39 | 40 | const subscriptionSchema: Schema = new Schema( 41 | { 42 | owner: { 43 | type: mongoose.Schema.Types.ObjectId, 44 | ref: "User", 45 | required: true, 46 | }, 47 | telegram_user_id: Number, 48 | subscriber: { 49 | type: mongoose.Schema.Types.ObjectId, 50 | ref: "Customer", 51 | }, 52 | currency_code: { type: String, default: "USD", required: true }, 53 | of_group: { 54 | type: mongoose.Schema.Types.ObjectId, 55 | ref: "Group", 56 | required: true, 57 | }, 58 | subscription_key: { type: String, required: true, unique: true }, 59 | anonymous_key: { type: String, required: true, unique: true }, 60 | amount: { 61 | type: Number, 62 | required: true, 63 | default: 0, 64 | }, 65 | billing: { 66 | cycle: { type: String, enum: ["month", "year"] }, 67 | billing_start: { type: Date }, 68 | billing_end: { type: Date }, 69 | }, 70 | status: { 71 | type: String, 72 | required: true, 73 | enum: ["activated", "canceled"], 74 | }, 75 | gateway: { 76 | provider: { 77 | type: String, 78 | enum: ["stripe", "paddle"], 79 | }, 80 | paddle: { 81 | price_id: { type: String }, 82 | subscription: { 83 | id: { type: String }, 84 | entity_type: { type: String }, 85 | }, 86 | }, 87 | }, 88 | }, 89 | { timestamps: true } 90 | ); 91 | 92 | export const Subscription: Model = 93 | mongoose.models.Subscription || 94 | mongoose.model("Subscription", subscriptionSchema); 95 | -------------------------------------------------------------------------------- /backend/src/lib/database/model/transaction.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema, Document, Model } from "mongoose"; 2 | 3 | export interface ITransaction { 4 | _id: string; 5 | status: string; 6 | anonymous_key: string; 7 | currency_code: string; 8 | of_group: string | mongoose.Schema.Types.ObjectId; 9 | price: 0; 10 | subscription: string | mongoose.Schema.Types.ObjectId; 11 | owner: string | mongoose.Schema.Types.ObjectId; 12 | createdAt: Date; 13 | updatedAt: Date; 14 | } 15 | 16 | // Extend Mongoose's Document and override _id type 17 | interface TransactionModel extends Omit, Document { 18 | _id: string; 19 | } 20 | 21 | const transactionSchema: Schema = new Schema( 22 | { 23 | price: { 24 | type: Number, 25 | }, 26 | currency_code: { type: String, default: "USD", required: true }, 27 | status: { type: String, default: "created" }, 28 | subscription: { 29 | type: mongoose.Schema.Types.ObjectId, 30 | ref: "Subscription", 31 | }, 32 | of_group: { 33 | type: mongoose.Schema.Types.ObjectId, 34 | ref: "Group", 35 | required: true, 36 | }, 37 | anonymous_key: { type: String, required: true, unique: true }, 38 | owner: { 39 | type: mongoose.Schema.Types.ObjectId, 40 | ref: "User", 41 | }, 42 | }, 43 | { timestamps: true } 44 | ); 45 | 46 | export const Transaction: Model = 47 | mongoose.models.Transaction || 48 | mongoose.model("Transaction", transactionSchema); 49 | -------------------------------------------------------------------------------- /backend/src/lib/database/model/wallet.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema, Document, Model } from "mongoose"; 2 | 3 | export interface IWallet { 4 | _id: string; 5 | owner: string | mongoose.Schema.Types.ObjectId; 6 | balance: number; 7 | withdraw: number; 8 | createdAt: Date; 9 | updatedAt: Date; 10 | } 11 | 12 | // Extend Mongoose's Document and override _id type 13 | interface WalletModel extends Omit, Document { 14 | _id: string; 15 | } 16 | 17 | const walletSchema: Schema = new Schema( 18 | { 19 | owner: { 20 | type: mongoose.Schema.Types.ObjectId, 21 | ref: "User", 22 | required: true, 23 | }, 24 | balance: { type: Number, required: true, default: 0 }, 25 | withdraw: { type: Number, required: true, default: 0 }, 26 | }, 27 | { timestamps: true } 28 | ); 29 | 30 | export const Wallet: Model = 31 | mongoose.models.Wallet || mongoose.model("Wallet", walletSchema); 32 | -------------------------------------------------------------------------------- /backend/src/lib/env.ts: -------------------------------------------------------------------------------- 1 | export const MONGODB_URI = process.env.MONGODB_URI || ""; 2 | 3 | export const CLIENT_DOMAIN = process.env.CLIENT_DOMAIN || ""; 4 | 5 | export const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET || ""; 6 | export const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID || ""; 7 | // Paypal 8 | export const PAYPAL_CLIENT_ID = process.env.PAYPAL_CLIENT_ID || ""; 9 | export const PAYPAL_SECRET_KEY = process.env.PAYPAL_SECRET_KEY || ""; 10 | export const PAYPAL_WEBHOOK_ID = process.env.PAYPAL_WEBHOOK_ID || ""; 11 | 12 | // Paddle 13 | export const PADDLE_API_KEY = process.env.PADDLE_API_KEY || ""; 14 | export const PADDLE_PRODUCT_ID = process.env.PADDLE_PRODUCT_ID || ""; 15 | 16 | export const PADDLE_SUBSCRIPTION_WEBHOOK_SECRET_KEY = 17 | process.env.PADDLE_SUBSCRIPTION_WEBHOOK_SECRET_KEY || ""; 18 | -------------------------------------------------------------------------------- /backend/src/lib/paddle/config.ts: -------------------------------------------------------------------------------- 1 | import { Environment, LogLevel, Paddle } from '@paddle/paddle-node-sdk' 2 | import { PADDLE_API_KEY } from '../env.js' 3 | 4 | const paddle = new Paddle(PADDLE_API_KEY, { 5 | environment: Environment.sandbox, 6 | logLevel: LogLevel.verbose 7 | }) 8 | 9 | export default paddle -------------------------------------------------------------------------------- /backend/src/lib/paypal/utils.ts: -------------------------------------------------------------------------------- 1 | import { paypalV1AxiosInstance } from "../axios/config.js"; 2 | import { PAYPAL_CLIENT_ID, PAYPAL_SECRET_KEY } from "../env.js"; 3 | 4 | export async function generatePaypalAccessToken(): Promise { 5 | const res = await paypalV1AxiosInstance.post( 6 | "/oauth2/token", 7 | { grant_type: "client_credentials" }, 8 | { 9 | auth: { 10 | username: PAYPAL_CLIENT_ID, 11 | password: PAYPAL_SECRET_KEY, 12 | }, 13 | headers: { 14 | "Content-Type": "application/x-www-form-urlencoded", 15 | }, 16 | } 17 | ); 18 | 19 | return res.data.access_token; 20 | } 21 | -------------------------------------------------------------------------------- /backend/src/lib/resend/config.ts: -------------------------------------------------------------------------------- 1 | import { Resend } from "resend"; 2 | 3 | const resend = new Resend(process.env.RESEND_API_KEY); 4 | 5 | export default resend; 6 | -------------------------------------------------------------------------------- /backend/src/lib/telegram/config.ts: -------------------------------------------------------------------------------- 1 | import TelegramBot from "node-telegram-bot-api"; 2 | 3 | const telegramToken = process.env.TELEGRAM_TOKEN || ""; 4 | 5 | const bot = new TelegramBot(telegramToken, { polling: true }); 6 | 7 | export default bot; 8 | -------------------------------------------------------------------------------- /backend/src/lib/telegram/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const REGISTER_GROUP = "Register Group" 2 | export const JOIN_GROUP = "Join Group" -------------------------------------------------------------------------------- /backend/src/lib/telegram/utils/index.ts: -------------------------------------------------------------------------------- 1 | import bot from "../config.js"; 2 | 3 | export const generateInviteLink = async (groupId: number, userId: number) => { 4 | await bot.unbanChatMember(groupId, userId, { only_if_banned: true }); 5 | 6 | const link = await bot.createChatInviteLink(groupId, { 7 | expire_date: Date.now() + 3600, 8 | member_limit: 1, 9 | }); 10 | 11 | return link; 12 | }; 13 | 14 | export async function removeUserFromGroup(chatId: number, userId: number) { 15 | if (!userId) { 16 | return; 17 | } 18 | 19 | await bot.banChatMember(chatId, userId); 20 | } 21 | -------------------------------------------------------------------------------- /backend/src/middleware/cors.middleware.ts: -------------------------------------------------------------------------------- 1 | import { cors } from "hono/cors"; 2 | import { CLIENT_DOMAIN } from "../lib/env.js"; 3 | 4 | const configCors = cors({ 5 | origin: CLIENT_DOMAIN, 6 | allowHeaders: ["Content-Type", "Authorization"], 7 | allowMethods: ["POST", "GET", "OPTIONS", "DELETE"], 8 | exposeHeaders: ["Content-Length"], 9 | maxAge: 600, 10 | credentials: true, 11 | }); 12 | 13 | export default configCors; 14 | -------------------------------------------------------------------------------- /backend/src/middleware/error.middleware.ts: -------------------------------------------------------------------------------- 1 | import { AxiosError } from "axios"; 2 | import type { Context } from "hono"; 3 | import { HTTPException } from "hono/http-exception"; 4 | import type { HTTPResponseError } from "hono/types"; 5 | import type { ContentfulStatusCode } from "hono/utils/http-status"; 6 | 7 | interface ErrorResponse { 8 | error: string; 9 | success: boolean; 10 | message: string; 11 | result: any; // For additional error context, if needed 12 | } 13 | 14 | const errorHandler = async (err: Error | HTTPResponseError, c: Context) => { 15 | // Log the error details 16 | console.error("Caught error in error handler:", err); 17 | 18 | let response: ErrorResponse; 19 | 20 | // Check for specific error types 21 | if (err instanceof ValidationError) { 22 | // Custom validation error response 23 | response = { 24 | success: false, 25 | error: "Validation Error", 26 | message: err.message, 27 | result: err.details, // Add field-specific details if available 28 | }; 29 | return c.json(response, err.status); 30 | } 31 | 32 | if (err instanceof AxiosError) { 33 | response = { 34 | success: false, 35 | error: "External API Error", 36 | message: err.response?.data?.message || err.message, 37 | result: err.response?.data || null, 38 | }; 39 | 40 | const statusCode = (err.status as ContentfulStatusCode) || 500; 41 | 42 | return c.json(response, { status: statusCode }); 43 | } 44 | 45 | if (err instanceof SyntaxError) { 46 | // Handle syntax errors (e.g., invalid JSON payloads) 47 | response = { 48 | success: false, 49 | error: "Bad Request", 50 | message: "Invalid JSON syntax in the request body.", 51 | result: null 52 | }; 53 | return c.json(response, { status: 400 }); 54 | } 55 | 56 | // Generic fallback error for unexpected issues 57 | response = { 58 | success: false, 59 | error: "Internal Server Error", 60 | message: "Something went wrong on our end. Please check result for more info.", 61 | result: err, 62 | }; 63 | return c.json(response, { status: 500 }); 64 | }; 65 | 66 | // Custom ValidationError class 67 | export class ValidationError extends HTTPException { 68 | details: Record; 69 | message: string; 70 | 71 | constructor( 72 | message: string, 73 | details: Record = {}, 74 | statusCode: ContentfulStatusCode = 500 75 | ) { 76 | const errorResponse = new Response( 77 | JSON.stringify({ 78 | error: "Validation Error", 79 | message, 80 | details, 81 | }), 82 | { 83 | status: statusCode, 84 | } 85 | ); 86 | super(statusCode, { res: errorResponse }); 87 | this.details = details; 88 | this.message = message; 89 | } 90 | } 91 | 92 | export default errorHandler; 93 | -------------------------------------------------------------------------------- /backend/src/middleware/session.middleware.ts: -------------------------------------------------------------------------------- 1 | import type { Context, Next } from "hono"; 2 | import { auth } from "../lib/better-auth/auth.js"; 3 | 4 | const addSession = async (c: Context, next: Next) => { 5 | const session = await auth.api.getSession({ headers: c.req.raw.headers }); 6 | 7 | if (!session) { 8 | c.set("user", null); 9 | c.set("session", null); 10 | return next(); 11 | } 12 | 13 | c.set("user", session.user); 14 | c.set("session", session.session); 15 | 16 | return next(); 17 | }; 18 | 19 | export default addSession; 20 | -------------------------------------------------------------------------------- /backend/src/middleware/unauthorized-access.middleware.ts: -------------------------------------------------------------------------------- 1 | import type { Context, Next } from "hono"; 2 | import { ValidationError } from "./error.middleware.js"; 3 | 4 | const sessionValidator = (c: Context, next: Next) => { 5 | const user = c.get("user"); 6 | const path = c.req.path; 7 | 8 | if (path.startsWith("/api/v1/dashboard") && !user) { 9 | throw new ValidationError( 10 | "Unauthorized access attempt detected.", 11 | { 12 | action: "access_protected_resource", 13 | requiredPermission: "user", 14 | receivedPermission: "unauthorized", 15 | }, 16 | 401 17 | ); 18 | } 19 | 20 | return next(); 21 | }; 22 | 23 | export default sessionValidator; 24 | -------------------------------------------------------------------------------- /backend/src/routes/crons/paypal.cron.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { Payout } from "../../lib/database/model/payout.model.js"; 3 | import { paypalV1AxiosInstance } from "../../lib/axios/config.js"; 4 | import { generatePaypalAccessToken } from "../../lib/paypal/utils.js"; 5 | import { io } from "../../index.js"; 6 | import { Wallet } from "../../lib/database/model/wallet.model.js"; 7 | 8 | const paypalCronRoute = new Hono(); 9 | 10 | paypalCronRoute.get("/paypal", async (c) => { 11 | const pendingPayouts = await Payout.find({ status: "PENDING" }); 12 | 13 | const token = await generatePaypalAccessToken(); 14 | 15 | for (const pendingPayout of pendingPayouts) { 16 | const payoutItemDetail = await paypalV1AxiosInstance.get( 17 | `/payments/payouts-item/${pendingPayout.paypal.payout_item_id}`, 18 | { 19 | headers: { 20 | Authorization: `Bearer ${token}`, 21 | }, 22 | } 23 | ); 24 | 25 | const payoutItemStatus = payoutItemDetail.data.transaction_status; 26 | 27 | await Payout.updateOne( 28 | { _id: pendingPayout._id }, 29 | { status: payoutItemStatus } 30 | ); 31 | 32 | if (payoutItemStatus === "SUCCESS") { 33 | await Wallet.updateOne( 34 | { owner: pendingPayout.owner }, 35 | { 36 | $inc: { 37 | balance: -pendingPayout.amount, 38 | withdraw: pendingPayout.amount, 39 | }, 40 | } 41 | ); 42 | } 43 | 44 | io.to(pendingPayout.owner as string).emit("update-payout", "refetch"); 45 | io.to(pendingPayout.owner as string).emit("update-wallet", "refetch"); 46 | } 47 | 48 | return c.text("ok", 200); 49 | }); 50 | 51 | export default paypalCronRoute; 52 | -------------------------------------------------------------------------------- /backend/src/routes/crons/validator.cron.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { Subscription } from "../../lib/database/model/subscription.model.js"; 3 | import { Group } from "../../lib/database/model/group.model.js"; 4 | import { removeUserFromGroup } from "../../lib/telegram/utils/index.js"; 5 | 6 | const validatorCron = new Hono(); 7 | 8 | validatorCron.get("/ban-user", async (c) => { 9 | const subscriptions = await Subscription.find({ status: "canceled" }); 10 | 11 | for (const subscription of subscriptions) { 12 | const group = await Group.findOne({ _id: subscription.of_group }); 13 | 14 | if (!group) { 15 | return c.text("error", 404); 16 | } 17 | 18 | await removeUserFromGroup(group.group_id, subscription.telegram_user_id); 19 | } 20 | 21 | return c.text("ok", 200); 22 | }); 23 | 24 | export default validatorCron; 25 | -------------------------------------------------------------------------------- /backend/src/routes/group.route.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import type { AuthSession } from "../lib/better-auth/auth-types.js"; 3 | import { Group, type IGroup } from "../lib/database/model/group.model.js"; 4 | import paddle from "../lib/paddle/config.js"; 5 | import { PADDLE_PRODUCT_ID } from "../lib/env.js"; 6 | 7 | const groupRoute = new Hono(); 8 | 9 | groupRoute.get("/groups", async (c) => { 10 | const user = c.get("user"); 11 | 12 | const groups = await Group.find({ owner: user!.id }).sort({ createdAt: -1 }); 13 | 14 | return c.json( 15 | { 16 | success: true, 17 | message: "User groups have been successfully retrieved.", 18 | result: groups, 19 | }, 20 | 200 21 | ); 22 | }); 23 | 24 | groupRoute.post("/groups", async (c) => { 25 | const user = c.get("user"); 26 | const { body } = await c.req.json(); 27 | 28 | const priceToUpdate = (Number(body.price) * 100).toString(); 29 | 30 | const groupInfo = await Group.findOne({ _id: body.id }); 31 | 32 | if (!groupInfo) { 33 | throw "Something went wrong. Please try again."; 34 | } 35 | 36 | let isPriceAvail: any; 37 | let updatedGroup: IGroup | null; 38 | 39 | try { 40 | isPriceAvail = await paddle.prices.get(groupInfo.price_id); 41 | } catch (error) { 42 | console.log("Price is not exist. Let's create a new price here"); 43 | } 44 | 45 | if (isPriceAvail) { 46 | const price = await paddle.prices.update(groupInfo.price_id, { 47 | unitPrice: { 48 | amount: priceToUpdate, 49 | currencyCode: "USD", 50 | }, 51 | }); 52 | 53 | updatedGroup = await Group.findOneAndUpdate( 54 | { _id: body.id }, 55 | { 56 | price: body.price, 57 | price_id: price.id, 58 | }, 59 | { new: true } 60 | ); 61 | } else { 62 | const price = await paddle.prices.create({ 63 | name: `Price for ${groupInfo?.name}`, 64 | productId: PADDLE_PRODUCT_ID, 65 | billingCycle: { 66 | interval: "month", 67 | frequency: 1, 68 | }, 69 | taxMode: "external", 70 | description: `Created by user ${user?.email}`, 71 | unitPrice: { 72 | amount: priceToUpdate, 73 | currencyCode: "USD", 74 | }, 75 | quantity: { 76 | minimum: 1, 77 | maximum: 9999999, 78 | }, 79 | }); 80 | 81 | updatedGroup = await Group.findOneAndUpdate( 82 | { _id: body.id }, 83 | { 84 | price: body.price, 85 | price_id: price.id, 86 | }, 87 | { new: true } 88 | ); 89 | } 90 | 91 | return c.json( 92 | { 93 | success: true, 94 | message: "Group has been updated successfully", 95 | result: updatedGroup, 96 | }, 97 | 200 98 | ); 99 | }); 100 | 101 | groupRoute.delete("/groups/:id", async (c) => { 102 | const id = c.req.param("id"); 103 | 104 | await Group.deleteOne({ _id: id }); 105 | 106 | return c.json( 107 | { 108 | success: true, 109 | message: "Group has been deleted successfully", 110 | result: id, 111 | }, 112 | 200 113 | ); 114 | }); 115 | 116 | export default groupRoute; 117 | -------------------------------------------------------------------------------- /backend/src/routes/order.route.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { Transaction } from "../lib/database/model/transaction.model.js"; 3 | import { Customer } from "../lib/database/model/customer.model.js"; 4 | 5 | const orderRoute = new Hono(); 6 | 7 | orderRoute.get("/order/:key", async (c) => { 8 | const key = c.req.param("key"); 9 | 10 | const transaction = await Transaction.findOne({ anonymous_key: key }); 11 | 12 | const customer = await Customer.findOne({ anonymous_key: key }); 13 | 14 | const result = { customer, transaction }; 15 | 16 | return c.json({ success: true, result }, 200); 17 | }); 18 | 19 | export default orderRoute; 20 | -------------------------------------------------------------------------------- /backend/src/routes/payout-history.route.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import type { AuthSession } from "../lib/better-auth/auth-types.js"; 3 | import { Payout } from "../lib/database/model/payout.model.js"; 4 | 5 | const payoutHistoryRoute = new Hono(); 6 | 7 | payoutHistoryRoute.get("/payout-history", async (c) => { 8 | const user = c.get("user")!; 9 | 10 | const last7Days = new Date(); 11 | last7Days.setDate(last7Days.getDate() - 7); 12 | 13 | const payout = await Payout.find({ 14 | owner: user.id, 15 | createdAt: { $gte: last7Days }, 16 | }); 17 | 18 | return c.json({ success: true, message: "", result: payout }); 19 | }); 20 | 21 | export default payoutHistoryRoute; 22 | -------------------------------------------------------------------------------- /backend/src/routes/public-group.route.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { Group } from "../lib/database/model/group.model.js"; 3 | 4 | const publicGroupRoute = new Hono(); 5 | 6 | publicGroupRoute.get("/groups/:id", async (c) => { 7 | const id = c.req.param("id"); 8 | 9 | const group = await Group.findOne({ _id: id }); 10 | 11 | return c.json( 12 | { 13 | success: true, 14 | message: "", 15 | result: group, 16 | }, 17 | 200 18 | ); 19 | }); 20 | 21 | export default publicGroupRoute; 22 | -------------------------------------------------------------------------------- /backend/src/routes/session.route.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import type { AuthSession } from "../lib/better-auth/auth-types.js"; 3 | 4 | const sessionRoute = new Hono(); 5 | 6 | sessionRoute.get("/session", async (c) => { 7 | const session = c.get("session"); 8 | const user = c.get("user"); 9 | 10 | if (!user) return c.body(null, 401); 11 | 12 | return c.json( 13 | { 14 | session, 15 | user, 16 | }, 17 | 200 18 | ); 19 | }); 20 | 21 | export default sessionRoute -------------------------------------------------------------------------------- /backend/src/routes/stats.route.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import type { AuthSession } from "../lib/better-auth/auth-types.js"; 3 | import { Group } from "../lib/database/model/group.model.js"; 4 | import { Customer } from "../lib/database/model/customer.model.js"; 5 | import { Transaction } from "../lib/database/model/transaction.model.js"; 6 | 7 | const statsRoute = new Hono(); 8 | 9 | statsRoute.get("/overview", async (c) => { 10 | const user = c.get("user")!; 11 | 12 | const last30Days = new Date(); 13 | const last7Days = new Date(); 14 | last30Days.setDate(last30Days.getDate() - 30); 15 | last7Days.setDate(last7Days.getDate() - 7); 16 | 17 | // 1. Calculate Total Revenue from Groups 18 | const groups = await Group.find({ 19 | owner: user.id, 20 | createdAt: { $gte: last30Days }, 21 | }); 22 | const earnings = groups.reduce((sum, group) => sum + group.revenue, 0); 23 | 24 | // 2. Count Transactions in the Last 30 Days 25 | const transactionCount = await Transaction.find({ 26 | owner: user.id, 27 | createdAt: { $gte: last7Days }, 28 | }).countDocuments(); 29 | 30 | const transactionDetails = await Transaction.find({ 31 | owner: user.id, 32 | createdAt: { $gte: last7Days }, 33 | }).sort({ createdAt: -1 }); 34 | 35 | // 3. Count Customers in the Last 30 Days 36 | const customerCount = await Customer.find({ 37 | owner: user.id, 38 | createdAt: { $gte: last7Days }, 39 | }).countDocuments(); 40 | 41 | const customerDetails = await Customer.find({ 42 | owner: user.id, 43 | createdAt: { $gte: last7Days }, 44 | }).sort({ createdAt: -1 }); 45 | 46 | // Format Results 47 | const result = { 48 | earnings: earnings || 0, 49 | totalCustomers: customerCount || 0, 50 | customerDetails: customerDetails || [], 51 | totalTransactions: transactionCount || 0, 52 | transactionDetails: transactionDetails || [], 53 | }; 54 | 55 | return c.json({ status: true, message: "", result }, 200); 56 | }); 57 | 58 | export default statsRoute; 59 | -------------------------------------------------------------------------------- /backend/src/routes/v1.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import groupRoute from "./group.route.js"; 3 | import sessionRoute from "./session.route.js"; 4 | import publicGroupRoute from "./public-group.route.js"; 5 | import paddleWebhookRoute from "./paddle-webhook.route.js"; 6 | import statsRoute from "./stats.route.js"; 7 | import orderRoute from "./order.route.js"; 8 | import paypalRoute from "./paypal-payout.route.js"; 9 | import walletRoute from "./wallet.route.js"; 10 | import payoutHistoryRoute from "./payout-history.route.js"; 11 | import paypalWebhook from "./paypal-webhook.route.js"; 12 | import validatorCron from "./crons/validator.cron.js"; 13 | import paypalCronRoute from "./crons/paypal.cron.js"; 14 | 15 | const routes = new Hono(); 16 | 17 | // Protected Route - Dashboard 18 | routes.route("/v1/dashboard", groupRoute); 19 | routes.route("/v1/dashboard", statsRoute); 20 | routes.route("/v1/dashboard", walletRoute); 21 | routes.route("/v1/dashboard", payoutHistoryRoute); 22 | 23 | // Protected Route - Payment 24 | routes.route("/v1/dashboard/paypal", paypalRoute); 25 | 26 | // Public Route - Groups for order 27 | routes.route("/v1", publicGroupRoute); 28 | routes.route("/v1", orderRoute); // For Thankyou page 29 | 30 | // Public Route - Paddle webhook 31 | routes.route("/v1/webhook", paddleWebhookRoute); 32 | routes.route("/v1/webhook", paypalWebhook); 33 | 34 | // Public Route - To get session on client side 35 | routes.route("/v1/user", sessionRoute); 36 | 37 | // Crons 38 | routes.route("/v1/cron", validatorCron) // To ban user 39 | routes.route("/v1/cron", paypalCronRoute) 40 | 41 | export default routes; 42 | -------------------------------------------------------------------------------- /backend/src/routes/wallet.route.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import type { AuthSession } from "../lib/better-auth/auth-types.js"; 3 | import { Wallet } from "../lib/database/model/wallet.model.js"; 4 | 5 | const walletRoute = new Hono(); 6 | 7 | walletRoute.get("/wallet", async (c) => { 8 | const user = c.get("user")!; 9 | 10 | const wallet = await Wallet.findOne({ owner: user.id }); 11 | 12 | return c.json({ success: true, message: "", result: wallet }, 200); 13 | }); 14 | 15 | export default walletRoute; 16 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "NodeNext", 5 | "strict": true, 6 | "outDir": "dist", 7 | "verbatimModuleSyntax": true, 8 | "skipLibCheck": true, 9 | "types": [ 10 | "node" 11 | ], 12 | "jsx": "react-jsx", 13 | // "jsxImportSource": "react" 14 | "jsxImportSource": "hono/jsx", 15 | } 16 | } -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | 43 | certificates -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /frontend/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devmdfaiz/group-gains/4d985225ebcebe7df13ac26a85bb3aa44067f7a7/frontend/bun.lockb -------------------------------------------------------------------------------- /frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /frontend/env.examle: -------------------------------------------------------------------------------- 1 | # General 2 | NEXT_PUBLIC_SERVER_BASE_URL=http://localhost:8080 3 | NEXT_PUBLIC_APP_BASE_URL=https://localhost:3000 4 | 5 | # Paddle 6 | NEXT_PUBLIC_PADDLE_CLIENT_TOKEN= -------------------------------------------------------------------------------- /frontend/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /frontend/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack --experimental-https", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@hookform/resolvers": "^3.10.0", 13 | "@paddle/paddle-js": "^1.3.3", 14 | "@radix-ui/react-accordion": "^1.2.2", 15 | "@radix-ui/react-alert-dialog": "^1.1.5", 16 | "@radix-ui/react-aspect-ratio": "^1.1.1", 17 | "@radix-ui/react-avatar": "^1.1.2", 18 | "@radix-ui/react-checkbox": "^1.1.3", 19 | "@radix-ui/react-collapsible": "^1.1.2", 20 | "@radix-ui/react-context-menu": "^2.2.5", 21 | "@radix-ui/react-dialog": "^1.1.5", 22 | "@radix-ui/react-dropdown-menu": "^2.1.5", 23 | "@radix-ui/react-hover-card": "^1.1.5", 24 | "@radix-ui/react-label": "^2.1.1", 25 | "@radix-ui/react-menubar": "^1.1.5", 26 | "@radix-ui/react-navigation-menu": "^1.2.4", 27 | "@radix-ui/react-popover": "^1.1.5", 28 | "@radix-ui/react-progress": "^1.1.1", 29 | "@radix-ui/react-radio-group": "^1.2.2", 30 | "@radix-ui/react-scroll-area": "^1.2.2", 31 | "@radix-ui/react-select": "^2.1.5", 32 | "@radix-ui/react-separator": "^1.1.1", 33 | "@radix-ui/react-slider": "^1.2.2", 34 | "@radix-ui/react-slot": "^1.1.1", 35 | "@radix-ui/react-switch": "^1.1.2", 36 | "@radix-ui/react-tabs": "^1.1.2", 37 | "@radix-ui/react-toast": "^1.2.5", 38 | "@radix-ui/react-toggle": "^1.1.1", 39 | "@radix-ui/react-toggle-group": "^1.1.1", 40 | "@radix-ui/react-tooltip": "^1.1.7", 41 | "@remixicon/react": "^4.6.0", 42 | "@tanstack/react-query": "^5.64.2", 43 | "axios": "^1.7.9", 44 | "better-auth": "^1.1.14", 45 | "class-variance-authority": "^0.7.1", 46 | "clsx": "^2.1.1", 47 | "cmdk": "1.0.0", 48 | "date-fns": "^4.1.0", 49 | "embla-carousel-react": "^8.5.2", 50 | "input-otp": "^1.4.2", 51 | "lucide-react": "^0.474.0", 52 | "next": "15.1.4", 53 | "next-themes": "^0.4.4", 54 | "react": "^19.0.0", 55 | "react-day-picker": "8.10.1", 56 | "react-dom": "^19.0.0", 57 | "react-hook-form": "^7.54.2", 58 | "react-resizable-panels": "^2.1.7", 59 | "recharts": "^2.15.0", 60 | "socket.io-client": "^4.8.1", 61 | "sonner": "^1.7.2", 62 | "tailwind-merge": "^2.6.0", 63 | "tailwindcss-animate": "^1.0.7", 64 | "vaul": "^1.1.2", 65 | "zod": "^3.24.1" 66 | }, 67 | "devDependencies": { 68 | "@eslint/eslintrc": "^3", 69 | "@types/node": "^20", 70 | "@types/react": "^19", 71 | "@types/react-dom": "^19", 72 | "eslint": "^9", 73 | "eslint-config-next": "15.1.4", 74 | "postcss": "^8", 75 | "tailwindcss": "^3.4.1", 76 | "tailwindcss-debug-screens": "^2.2.1", 77 | "typescript": "^5" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /frontend/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /frontend/public/paypal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devmdfaiz/group-gains/4d985225ebcebe7df13ac26a85bb3aa44067f7a7/frontend/public/paypal.png -------------------------------------------------------------------------------- /frontend/public/telegram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devmdfaiz/group-gains/4d985225ebcebe7df13ac26a85bb3aa44067f7a7/frontend/public/telegram.png -------------------------------------------------------------------------------- /frontend/src/@types/models.ts: -------------------------------------------------------------------------------- 1 | export interface IGroup { 2 | _id: string; 3 | group_id: string; // Use snake_case here 4 | name: string; 5 | price_id: string; 6 | revenue: number; 7 | price: number; 8 | owner: string; 9 | createdAt: Date; 10 | updatedAt: Date; 11 | } 12 | 13 | export interface ICustomer { 14 | _id: string; 15 | name: string; 16 | anonymous_key: string; 17 | email: string; 18 | subscription: string; 19 | owner: string; 20 | createdAt: Date; 21 | updatedAt: Date; 22 | } 23 | 24 | export interface ITransaction { 25 | _id: string; 26 | status: "paid" | "created"; 27 | anonymous_key: string; 28 | currency_code: string; 29 | of_group: string; 30 | price: number; 31 | subscription: string; 32 | owner: string; 33 | createdAt: Date; 34 | updatedAt: Date; 35 | } 36 | 37 | export interface IIntegration { 38 | _id: string; 39 | owner: string; 40 | paypal: { 41 | email: string; 42 | currency: string; 43 | }; 44 | createdAt: Date; 45 | updatedAt: Date; 46 | } 47 | 48 | export interface IWallet { 49 | _id: string; 50 | owner: string; 51 | balance: number; 52 | withdraw: number; 53 | createdAt: Date; 54 | updatedAt: Date; 55 | } 56 | 57 | export interface IPayout { 58 | _id: string; 59 | owner: string; 60 | amount: number; 61 | paypal: { 62 | payout_id: string; 63 | }; 64 | status: string; 65 | createdAt: Date; 66 | updatedAt: Date; 67 | } -------------------------------------------------------------------------------- /frontend/src/@types/response.ts: -------------------------------------------------------------------------------- 1 | import { ICustomer, ITransaction } from "./models"; 2 | 3 | export interface ApiResponse { 4 | success: boolean; 5 | error?: string; 6 | message: string; 7 | result: T; 8 | } 9 | 10 | export interface PayPalLink { 11 | href: string; 12 | rel: string; 13 | method: string; 14 | description: string; 15 | } 16 | 17 | export interface PayPalPartnerResponse { 18 | links: PayPalLink[]; 19 | } 20 | 21 | export type PaypalPartnerApiResponse = ApiResponse; 22 | 23 | export interface OverviewResponse { 24 | earnings: number; 25 | totalCustomers: number; 26 | customerDetails: ICustomer[]; 27 | totalTransactions: number; 28 | transactionDetails: ITransaction[]; 29 | } 30 | 31 | export interface ThankYouResponse { 32 | customer: ICustomer; 33 | transaction: ITransaction; 34 | } 35 | -------------------------------------------------------------------------------- /frontend/src/app/(auth)/_components/auth-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { P, paragraphVariants } from "@/components/custom/p"; 4 | import { Button } from "@/components/ui/button"; 5 | import { 6 | Card, 7 | CardContent, 8 | CardDescription, 9 | CardHeader, 10 | CardTitle, 11 | } from "@/components/ui/card"; 12 | import Link from "next/link"; 13 | 14 | import { RiGoogleFill, RiLoader3Fill } from "@remixicon/react"; 15 | import { authClient } from "@/lib/better-auth/auth-client"; 16 | import { useToast } from "@/hooks/use-toast"; 17 | import { useState } from "react"; 18 | import { APP_DOMAIN } from "@/lib/env"; 19 | 20 | interface Props { 21 | action: "Sign In" | "Sign Up"; 22 | } 23 | 24 | const AuthForm = ({ action }: Props) => { 25 | const { toast } = useToast(); 26 | const [isLoading, setIsLoading] = useState(false); 27 | 28 | return ( 29 | 30 | 31 | 34 | {action} 35 | 36 | {action} to access your account 37 | 38 | 39 | 78 | 79 |

85 | {action === "Sign In" ? ( 86 | <> 87 | Don't have an account?{" "} 88 | 89 | Sign Up 90 | 91 | 92 | ) : ( 93 | <> 94 | Already have an account?{" "} 95 | 96 | Sign In 97 | 98 | 99 | )} 100 |

101 |
102 |
103 | ); 104 | }; 105 | 106 | export default AuthForm; 107 | -------------------------------------------------------------------------------- /frontend/src/app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { P } from "@/components/custom/p"; 2 | import { cn } from "@/lib/utils"; 3 | import React from "react"; 4 | 5 | interface Props { 6 | children: React.ReactNode; 7 | } 8 | 9 | const Layout = ({ children }: Props) => { 10 | return ( 11 |
12 |
13 |
14 |

Group Gains

15 |

16 | The only platform that is able monetize your telegram group. 17 |

18 |
19 |
20 | 21 |
22 | {children} 23 |
24 |
25 | ); 26 | }; 27 | 28 | export default Layout; 29 | -------------------------------------------------------------------------------- /frontend/src/app/(auth)/sign-in/page.tsx: -------------------------------------------------------------------------------- 1 | import AuthForm from "../_components/auth-form"; 2 | 3 | const SignInPage = () => { 4 | return ; 5 | }; 6 | 7 | export default SignInPage; 8 | -------------------------------------------------------------------------------- /frontend/src/app/(auth)/sign-up/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import AuthForm from "../_components/auth-form"; 3 | 4 | const SignUpPage = () => { 5 | return ; 6 | }; 7 | 8 | export default SignUpPage; 9 | -------------------------------------------------------------------------------- /frontend/src/app/(checkout)/_components/checkout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { IGroup } from "@/@types/models"; 4 | import usePaddle from "@/hooks/use-paddle"; 5 | import { APP_DOMAIN } from "@/lib/env"; 6 | import { RiLoader3Fill } from "@remixicon/react"; 7 | import { useEffect, useState } from "react"; 8 | 9 | const Checkout = ({ 10 | group, 11 | anonymousKey, 12 | }: { 13 | group: IGroup; 14 | anonymousKey: string; 15 | }) => { 16 | const paddle = usePaddle(); 17 | const [isLoading, setIsLoading] = useState(false); 18 | 19 | useEffect(() => { 20 | setIsLoading(true); 21 | paddle?.Checkout.open({ 22 | settings: { 23 | displayMode: "overlay", 24 | theme: "light", 25 | locale: "en", 26 | successUrl: `${APP_DOMAIN}/thank-you/${anonymousKey}`, 27 | }, 28 | items: [ 29 | { 30 | priceId: group.price_id, 31 | quantity: 1, 32 | }, 33 | ], 34 | customData: { 35 | entityType: "subscription", 36 | anonymousKey, 37 | group: { 38 | id: group._id, 39 | owner: group.owner, 40 | entityType: "group", 41 | amount: group.price, 42 | priceId: group.price_id, 43 | }, 44 | }, 45 | }); 46 | setIsLoading(false); 47 | }, [paddle]); 48 | 49 | return ( 50 |
51 | {isLoading && } 52 |
53 | ); 54 | }; 55 | 56 | export default Checkout; 57 | -------------------------------------------------------------------------------- /frontend/src/app/(checkout)/checkout/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import Checkout from "../../_components/checkout"; 2 | import { findOneGroup } from "@/lib/fetch/group.fetch"; 3 | import { P } from "@/components/custom/p"; 4 | import { randomBytes } from "crypto"; 5 | 6 | interface Props { 7 | params: Promise<{ id: string }>; 8 | } 9 | 10 | const CheckoutPage = async ({ params }: Props) => { 11 | const groupId = (await params).id; 12 | const anonymousKey = randomBytes(10).toString("hex"); 13 | 14 | const res = await findOneGroup(groupId); 15 | 16 | const group = res.result; 17 | 18 | if (!group) return

{res.message}

; 19 | 20 | return ; 21 | }; 22 | 23 | export default CheckoutPage; 24 | -------------------------------------------------------------------------------- /frontend/src/app/(checkout)/checkout/loading.tsx: -------------------------------------------------------------------------------- 1 | import { RiLoader3Fill } from "@remixicon/react"; 2 | 3 | export default function Loading() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/_components/app-sidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Sidebar, 5 | SidebarContent, 6 | SidebarGroup, 7 | SidebarGroupContent, 8 | SidebarGroupLabel, 9 | SidebarMenu, 10 | SidebarMenuButton, 11 | SidebarMenuItem, 12 | } from "@/components/ui/sidebar"; 13 | import { 14 | RiBubbleChartFill, 15 | RiMoneyDollarCircleFill, 16 | RiUserCommunityFill, 17 | } from "@remixicon/react"; 18 | import Link from "next/link"; 19 | import { cn } from "@/lib/utils"; 20 | import { paragraphVariants } from "@/components/custom/p"; 21 | import { usePathname } from "next/navigation"; 22 | 23 | // Menu items. 24 | const items = [ 25 | { 26 | title: "Overview", 27 | url: "/dashboard/overview", 28 | icon: RiBubbleChartFill, 29 | }, 30 | { 31 | title: "Groups", 32 | url: "/dashboard/groups", 33 | icon: RiUserCommunityFill, 34 | }, 35 | { 36 | title: "Payout", 37 | url: "/dashboard/payout", 38 | icon: RiMoneyDollarCircleFill, 39 | }, 40 | ]; 41 | 42 | export function AppSidebar() { 43 | const pathname = usePathname(); 44 | return ( 45 | 46 | 47 | 48 | Navigation 49 | 50 | 51 | {items.map((item) => ( 52 | 53 | 65 | 66 | 67 | {item.title} 68 | 69 | 70 | 71 | ))} 72 | 73 | 74 | 75 | 76 | 77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/_components/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { paragraphVariants } from "@/components/custom/p"; 4 | import { 5 | Breadcrumb, 6 | BreadcrumbItem, 7 | BreadcrumbLink, 8 | BreadcrumbList, 9 | BreadcrumbPage, 10 | BreadcrumbSeparator, 11 | } from "@/components/ui/breadcrumb"; 12 | import { cn } from "@/lib/utils"; 13 | import { usePathname } from "next/navigation"; 14 | 15 | const DashboardBreadcrumb = () => { 16 | const pathname = usePathname(); // pathname: /dashboard/images ["dashboard", "images"] 17 | 18 | const paths = pathname.split("/").filter((path) => path !== ""); 19 | 20 | return ( 21 | 22 | 23 | {paths.length > 1 && 24 | paths.map((path, i) => { 25 | const isLast = i === paths.length - 1; 26 | const currentPath = paths.find((_, index) => index === i); 27 | 28 | return ( 29 |
30 | {!isLast ? ( 31 | <> 32 | 33 | 43 | {path} 44 | 45 | 46 | 47 | 48 | ) : ( 49 | 50 | 59 | {path} 60 | 61 | 62 | )} 63 |
64 | ); 65 | })} 66 |
67 |
68 | ); 69 | }; 70 | 71 | export default DashboardBreadcrumb; 72 | -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/_components/header-profile.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Skeleton } from "@/components/ui/skeleton"; 4 | import { signOut, useSession } from "@/lib/better-auth/auth-client"; 5 | import { useRouter } from "next/navigation"; 6 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 7 | 8 | import { 9 | DropdownMenu, 10 | DropdownMenuContent, 11 | DropdownMenuItem, 12 | DropdownMenuLabel, 13 | DropdownMenuSeparator, 14 | DropdownMenuTrigger, 15 | } from "@/components/ui/dropdown-menu"; 16 | import { cn } from "@/lib/utils"; 17 | import { paragraphVariants } from "@/components/custom/p"; 18 | import { RiClipboardFill, RiLogoutCircleFill } from "@remixicon/react"; 19 | import { toast } from "sonner"; 20 | 21 | const HeaderProfile = () => { 22 | const session = useSession(); 23 | const router = useRouter(); 24 | 25 | const { isPending, data } = session; 26 | return ( 27 | <> 28 | {isPending && } 29 | 30 | 31 | 32 | {!isPending && ( 33 | 34 | 35 | 36 | {(data?.user?.name as string)?.slice(0, 1)} 37 | 38 | 39 | )} 40 | 41 | 42 | 47 | Action 48 | 49 | 50 | { 53 | navigator.clipboard 54 | .writeText(data?.user.id || "") 55 | .then(() => 56 | toast("Copied!", { 57 | description: "User key is copied", 58 | }) 59 | ) 60 | .catch((err) => toast("Failed!", { description: `${err}` })); 61 | }} 62 | > 63 | 64 | 69 | User Key 70 | 71 | 72 | { 75 | await signOut(); 76 | 77 | router.push("/sign-in"); 78 | }} 79 | > 80 | 81 | 86 | Log Out 87 | 88 | 89 | 90 | 91 | 92 | ); 93 | }; 94 | 95 | export default HeaderProfile; 96 | -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/_components/header.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import SidebarTrigger from "@/components/custom/sidebar-trigger"; 4 | import DashboardBreadcrumb from "./breadcrumb"; 5 | import HeaderProfile from "./header-profile"; 6 | 7 | const DashboardHeader = () => { 8 | return ( 9 |
10 |
11 | 12 | 13 |
14 | 15 |
16 | 17 |
18 |
19 | ); 20 | }; 21 | 22 | export default DashboardHeader; 23 | -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/_components/page-header.tsx: -------------------------------------------------------------------------------- 1 | import { P } from "@/components/custom/p"; 2 | 3 | const PageHeader = ({ 4 | title, 5 | description, 6 | }: { 7 | title: string; 8 | description: string; 9 | }) => { 10 | return ( 11 |
12 |

{title}

13 |

{description}

14 |
15 | ); 16 | }; 17 | 18 | export default PageHeader; 19 | -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/dashboard/groups/page.tsx: -------------------------------------------------------------------------------- 1 | import getServerSession from "@/lib/better-auth/server-session"; 2 | import PageHeader from "../../_components/page-header"; 3 | import GroupCardWrapper from "./_components/group-card"; 4 | import { redirect } from "next/navigation"; 5 | 6 | const GroupsPage = async () => { 7 | const session = await getServerSession(); 8 | 9 | if (!session) return redirect("/sign-in"); 10 | 11 | return ( 12 | <> 13 | {/* Header */} 14 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default GroupsPage; 25 | -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/dashboard/overview/_components/customers-table.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { P } from "@/components/custom/p"; 4 | import { Card, CardContent } from "@/components/ui/card"; 5 | import { 6 | Table, 7 | TableBody, 8 | TableCell, 9 | TableHead, 10 | TableHeader, 11 | TableRow, 12 | } from "@/components/ui/table"; 13 | import { Heart } from "lucide-react"; 14 | import { QueryRes } from "./overview"; 15 | import { format } from "date-fns"; 16 | import { Skeleton } from "@/components/ui/skeleton"; 17 | 18 | const CustomersTable = ({ query }: { query: QueryRes }) => { 19 | const { data: res, isLoading, error } = query; 20 | 21 | let customers = res?.result?.customerDetails; 22 | 23 | if (error) { 24 | customers = []; 25 | } 26 | return ( 27 | 28 | 29 |
30 |

Recent Customers

31 |
32 | {isLoading && ( 33 |
34 | {Array.from({ length: 4 }).map((_, i) => ( 35 | 36 | ))} 37 |
38 | )} 39 | {customers?.length === 0 ? ( 40 |
41 |
42 | 43 |
44 |

No customers yet

45 |

51 | Share your page with your audience to get started. 52 |

53 |
54 | ) : ( 55 | <> 56 | {!isLoading && ( 57 | 58 | 59 | 60 | Email 61 | Date 62 | 63 | 64 | 65 | {customers && 66 | customers.map((customer) => ( 67 | 68 | 69 | {customer.email} 70 | 71 | 72 | {format(customer.createdAt, "dd-MMM-yyyy")} 73 | 74 | 75 | ))} 76 | 77 |
78 | )} 79 | 80 | )} 81 |
82 |
83 | ); 84 | }; 85 | 86 | export default CustomersTable; 87 | -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/dashboard/overview/_components/overview-profile.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import PageHeader from "@/app/(dashboard)/_components/page-header"; 4 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 5 | import { Skeleton } from "@/components/ui/skeleton"; 6 | import { useSession } from "@/lib/better-auth/auth-client"; 7 | 8 | const OverviewProfile = () => { 9 | const { data, isPending } = useSession(); 10 | 11 | return ( 12 | <> 13 | {isPending && ( 14 |
15 | 16 |
17 | 18 | 19 |
20 |
21 | )} 22 | 23 | {!isPending && ( 24 |
25 | 26 | 29 | 30 | 31 | 32 | 33 | 37 |
38 | )} 39 | 40 | ); 41 | }; 42 | 43 | export default OverviewProfile; 44 | -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/dashboard/overview/_components/overview-stats.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { P } from "@/components/custom/p"; 4 | import { Card, CardContent } from "@/components/ui/card"; 5 | import { DollarSign, Users } from "lucide-react"; 6 | import { QueryRes } from "./overview"; 7 | import { Skeleton } from "@/components/ui/skeleton"; 8 | 9 | const OverviewStats = ({ query }: { query: QueryRes }) => { 10 | const { data: res, isLoading, error } = query; 11 | 12 | let earnings = res?.result?.earnings; 13 | let totalCustomers = res?.result?.totalCustomers; 14 | let totalTransactions = res?.result?.totalTransactions; 15 | 16 | if (error) { 17 | earnings = 0; 18 | totalCustomers = 0; 19 | totalTransactions = 0; 20 | } 21 | return ( 22 | 23 | 24 |
25 |

Earnings

26 |
27 | {!isLoading ? ( 28 |

29 | ${earnings} 30 |

31 | ) : ( 32 | 33 | )} 34 |

35 | Last 30 days 36 |

37 |
38 |
39 |
40 |
41 |
42 | 43 |
44 |
45 | {!isLoading ? ( 46 |

47 | {totalCustomers} 48 |

49 | ) : ( 50 | 51 | )} 52 |

53 | Customers 54 |

55 |
56 |
57 |
58 |
59 | 60 |
61 |
62 | {!isLoading ? ( 63 |

64 | {totalTransactions} 65 |

66 | ) : ( 67 | 68 | )} 69 |

70 | Transaction 71 |

72 |
73 |
74 |
75 |
76 |
77 | ); 78 | }; 79 | 80 | export default OverviewStats; 81 | -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/dashboard/overview/_components/overview.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import OverviewProfile from "./overview-profile"; 4 | import CustomersTable from "./customers-table"; 5 | import TransactionTable from "./transaction-table"; 6 | import OverviewStats from "./overview-stats"; 7 | import { 8 | useQuery, 9 | useQueryClient, 10 | UseQueryResult, 11 | } from "@tanstack/react-query"; 12 | import { axiosDashboardInstance } from "@/lib/axios/config"; 13 | import { ApiResponse, OverviewResponse } from "@/@types/response"; 14 | import { toast } from "sonner"; 15 | import useWebsocket from "@/hooks/use-websocket"; 16 | import socket from "@/lib/socket/config"; 17 | 18 | export type QueryRes = UseQueryResult< 19 | ApiResponse, 20 | Error 21 | >; 22 | 23 | const Overview = ({ userId }: { userId: string }) => { 24 | const queyClient = useQueryClient(); 25 | 26 | const query = useQuery({ 27 | queryKey: ["overview"], 28 | queryFn: async () => 29 | ( 30 | await axiosDashboardInstance.get>( 31 | "/overview" 32 | ) 33 | ).data, 34 | }); 35 | 36 | // webhook/paddle 37 | useWebsocket(() => { 38 | socket.on("update-overview", () => { 39 | queyClient.invalidateQueries({ queryKey: ["overview"] }); 40 | }); 41 | }, userId); 42 | 43 | if (query.error) { 44 | toast("Error", { 45 | description: query.data?.message, 46 | }); 47 | } 48 | 49 | return ( 50 |
51 |
52 | {/* Header */} 53 | 54 | 55 | {/* Stats Overview */} 56 | 57 | 58 |
59 | {/* Customers Section */} 60 | 61 | 62 | {/* Transactions Section */} 63 | 64 |
65 |
66 |
67 | ); 68 | }; 69 | 70 | export default Overview; 71 | -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/dashboard/overview/page.tsx: -------------------------------------------------------------------------------- 1 | import getServerSession from "@/lib/better-auth/server-session"; 2 | import Overview from "./_components/overview"; 3 | import { redirect } from "next/navigation"; 4 | 5 | const OverviewPage = async () => { 6 | const session = await getServerSession(); 7 | 8 | if (!session) return redirect("/sign-in"); 9 | 10 | return ; 11 | }; 12 | 13 | export default OverviewPage; 14 | -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | const DashboardPage = () => redirect("/dashboard/overview"); 4 | 5 | export default DashboardPage; 6 | -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/dashboard/payout/_components/payout.tsx: -------------------------------------------------------------------------------- 1 | import ManagePayout from "./manage-payout"; 2 | import BalanceCard from "./balance-card"; 3 | import PayoutHistory from "./payout-history"; 4 | 5 | function Payout({ userId }: { userId: string }) { 6 | return ( 7 |
8 | {/* Connected Status */} 9 | 10 | 11 | {/* Balance Card */} 12 | 13 | 14 | {/* Payout History */} 15 | 16 |
17 | ); 18 | } 19 | 20 | export default Payout; 21 | -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/dashboard/payout/page.tsx: -------------------------------------------------------------------------------- 1 | import getServerSession from "@/lib/better-auth/server-session"; 2 | import PageHeader from "../../_components/page-header"; 3 | import Payout from "./_components/payout"; 4 | import { redirect } from "next/navigation"; 5 | 6 | const PayoutPage = async () => { 7 | const session = await getServerSession(); 8 | 9 | if (!session) return redirect("/sign-in"); 10 | return ( 11 | <> 12 | 16 | 17 | 18 | ); 19 | }; 20 | 21 | export default PayoutPage; 22 | -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { SidebarProvider } from "@/components/ui/sidebar"; 2 | import React from "react"; 3 | import { AppSidebar } from "./_components/app-sidebar"; 4 | import DashboardHeader from "./_components/header"; 5 | 6 | interface Props { 7 | children: React.ReactNode; 8 | } 9 | 10 | const Layout = ({ children }: Props) => { 11 | return ( 12 |
13 | 14 | 15 |
16 | 17 |
18 | {children} 19 |
20 |
21 |
22 |
23 | ); 24 | }; 25 | 26 | export default Layout; 27 | -------------------------------------------------------------------------------- /frontend/src/app/(thank-you)/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Layout = ({ children }: { children: React.ReactNode }) => { 4 | return ( 5 |
6 |
{children}
7 |
8 | ); 9 | }; 10 | 11 | export default Layout; 12 | -------------------------------------------------------------------------------- /frontend/src/app/(thank-you)/thank-you/[key]/page.tsx: -------------------------------------------------------------------------------- 1 | import ThankYou from "../../_components/thak-you"; 2 | 3 | interface Props { 4 | params: Promise<{ key: string }>; 5 | } 6 | 7 | const ThankYouPage = async ({ params }: Props) => { 8 | const anonymousKey = (await params).key; 9 | return ; 10 | }; 11 | 12 | export default ThankYouPage; 13 | -------------------------------------------------------------------------------- /frontend/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devmdfaiz/group-gains/4d985225ebcebe7df13ac26a85bb3aa44067f7a7/frontend/src/app/favicon.ico -------------------------------------------------------------------------------- /frontend/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* body { 6 | font-family: Arial, Helvetica, sans-serif; 7 | } */ 8 | 9 | @layer base { 10 | :root { 11 | --background: 0 0% 100%; 12 | --foreground: 240 10% 3.9%; 13 | --card: 0 0% 100%; 14 | --card-foreground: 240 10% 3.9%; 15 | --popover: 0 0% 100%; 16 | --popover-foreground: 240 10% 3.9%; 17 | --primary: 142.1 76.2% 36.3%; 18 | --primary-foreground: 355.7 100% 97.3%; 19 | --secondary: 240 4.8% 95.9%; 20 | --secondary-foreground: 240 5.9% 10%; 21 | --muted: 240 4.8% 95.9%; 22 | --muted-foreground: 240 3.8% 46.1%; 23 | --accent: 240 4.8% 95.9%; 24 | --accent-foreground: 240 5.9% 10%; 25 | --destructive: 0 84.2% 60.2%; 26 | --destructive-foreground: 0 0% 98%; 27 | --border: 240 5.9% 90%; 28 | --input: 240 5.9% 90%; 29 | --ring: 142.1 76.2% 36.3%; 30 | --radius: 1rem; 31 | --chart-1: 12 76% 61%; 32 | --chart-2: 173 58% 39%; 33 | --chart-3: 197 37% 24%; 34 | --chart-4: 43 74% 66%; 35 | --chart-5: 27 87% 67%; 36 | } 37 | 38 | .dark { 39 | --background: 20 14.3% 4.1%; 40 | --foreground: 0 0% 95%; 41 | --card: 24 9.8% 10%; 42 | --card-foreground: 0 0% 95%; 43 | --popover: 0 0% 9%; 44 | --popover-foreground: 0 0% 95%; 45 | --primary: 142.1 70.6% 45.3%; 46 | --primary-foreground: 144.9 80.4% 10%; 47 | --secondary: 240 3.7% 15.9%; 48 | --secondary-foreground: 0 0% 98%; 49 | --muted: 0 0% 15%; 50 | --muted-foreground: 240 5% 64.9%; 51 | --accent: 12 6.5% 15.1%; 52 | --accent-foreground: 0 0% 98%; 53 | --destructive: 0 62.8% 30.6%; 54 | --destructive-foreground: 0 85.7% 97.3%; 55 | --border: 240 3.7% 15.9%; 56 | --input: 240 3.7% 15.9%; 57 | --ring: 142.4 71.8% 29.2%; 58 | --chart-1: 220 70% 50%; 59 | --chart-2: 160 60% 45%; 60 | --chart-3: 30 80% 55%; 61 | --chart-4: 280 65% 60%; 62 | --chart-5: 340 75% 55%; 63 | } 64 | } 65 | 66 | @layer base { 67 | * { 68 | @apply border-border; 69 | } 70 | body { 71 | @apply bg-background text-foreground; 72 | } 73 | h1 { 74 | @apply scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl; 75 | } 76 | 77 | h2 { 78 | @apply scroll-m-20 pb-2 text-3xl font-semibold tracking-tight first:mt-0; 79 | } 80 | 81 | h3 { 82 | @apply scroll-m-20 text-2xl font-semibold tracking-tight; 83 | } 84 | 85 | h4 { 86 | @apply scroll-m-20 text-xl font-semibold tracking-tight; 87 | } 88 | 89 | p { 90 | @apply leading-7 [&:not(:first-child)]:mt-6; 91 | } 92 | 93 | blockquote { 94 | @apply mt-6 border-l-2 pl-6 italic; 95 | } 96 | 97 | /* ul { 98 | @apply my-6 ml-6 list-disc [&>li]:mt-2; 99 | } */ 100 | 101 | code { 102 | @apply relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold; 103 | } 104 | 105 | .input { 106 | @apply border-none focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-transparent shadow-none !important; 107 | } 108 | 109 | .link { 110 | @apply text-blue-500 underline-offset-4 hover:underline; 111 | } 112 | 113 | ::-webkit-scrollbar { 114 | width: 8px; 115 | } 116 | 117 | ::-webkit-scrollbar-track { 118 | background: transparent; 119 | } 120 | 121 | ::-webkit-scrollbar-thumb { 122 | @apply bg-accent rounded-lg; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /frontend/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import "./globals.css"; 4 | import Provider from "@/context/provider"; 5 | import { Toaster as Sonner } from "@/components/ui/sonner"; 6 | import { cn } from "@/lib/utils"; 7 | 8 | const geistSans = Geist({ 9 | variable: "--font-geist-sans", 10 | subsets: ["latin"], 11 | }); 12 | 13 | const geistMono = Geist_Mono({ 14 | variable: "--font-geist-mono", 15 | subsets: ["latin"], 16 | }); 17 | 18 | export const metadata: Metadata = { 19 | title: "Create Next App", 20 | description: "Generated by create next app", 21 | }; 22 | 23 | export default function RootLayout({ 24 | children, 25 | }: Readonly<{ 26 | children: React.ReactNode; 27 | }>) { 28 | return ( 29 | 30 | 38 | {children} 39 | 40 | 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /frontend/src/components/custom/button-styles.ts: -------------------------------------------------------------------------------- 1 | export const lift = 2 | "w-full shadow-md hover:shadow-lg hover:shadow-primary/50 shadow-primary/50 transition-all ease-in-out duration-200 bg-primary text-white"; 3 | -------------------------------------------------------------------------------- /frontend/src/components/custom/p.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import { cva, VariantProps } from "class-variance-authority"; 3 | 4 | const paragraphVariants = cva("leading-7 [&:not(:first-child)]:mt-0", { 5 | variants: { 6 | variant: { 7 | default: "", 8 | lead: "text-xl text-muted-foreground", 9 | muted: "text-sm text-muted-foreground", 10 | child: "[&:not(:first-child)]:mt-6", 11 | }, 12 | size: { 13 | default: "", 14 | small: "text-sm font-medium leading-none", 15 | large: "text-lg font-semibold", 16 | medium: "text-base", 17 | }, 18 | weight: { 19 | default: "", 20 | bold: "font-bold", 21 | medium: "font-medium", 22 | light: "font-light", 23 | }, 24 | }, 25 | defaultVariants: { 26 | size: "default", 27 | variant: "default", 28 | weight: "default", 29 | }, 30 | }); 31 | 32 | // Extend with native attributes 33 | interface ParagraphProps 34 | extends React.HTMLAttributes, 35 | VariantProps {} 36 | 37 | const P = ({ className, variant, size, weight, ...props }: ParagraphProps) => { 38 | return ( 39 |

43 | ); 44 | }; 45 | 46 | export { P, paragraphVariants }; 47 | -------------------------------------------------------------------------------- /frontend/src/components/custom/sidebar-trigger.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import { Button } from "../ui/button"; 3 | import { useSidebar } from "../ui/sidebar"; 4 | import { RiSidebarFoldFill, RiSidebarUnfoldFill } from "@remixicon/react"; 5 | 6 | const SidebarTrigger = () => { 7 | const { toggleSidebar, state } = useSidebar(); 8 | 9 | return ( 10 | 22 | ); 23 | }; 24 | 25 | export default SidebarTrigger -------------------------------------------------------------------------------- /frontend/src/components/icons/kebab.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | export const KebabMenuIcon = ({ className }: { className?: string }) => { 4 | return ( 5 | 17 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /frontend/src/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion" 5 | import { ChevronDown } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Accordion = AccordionPrimitive.Root 10 | 11 | const AccordionItem = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 20 | )) 21 | AccordionItem.displayName = "AccordionItem" 22 | 23 | const AccordionTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, children, ...props }, ref) => ( 27 | 28 | svg]:rotate-180", 32 | className 33 | )} 34 | {...props} 35 | > 36 | {children} 37 | 38 | 39 | 40 | )) 41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName 42 | 43 | const AccordionContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, children, ...props }, ref) => ( 47 | 52 |

{children}
53 | 54 | )) 55 | AccordionContent.displayName = AccordionPrimitive.Content.displayName 56 | 57 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } 58 | -------------------------------------------------------------------------------- /frontend/src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ) 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 |
32 | )) 33 | Alert.displayName = "Alert" 34 | 35 | const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |
44 | )) 45 | AlertTitle.displayName = "AlertTitle" 46 | 47 | const AlertDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )) 57 | AlertDescription.displayName = "AlertDescription" 58 | 59 | export { Alert, AlertTitle, AlertDescription } 60 | -------------------------------------------------------------------------------- /frontend/src/components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" 4 | 5 | const AspectRatio = AspectRatioPrimitive.Root 6 | 7 | export { AspectRatio } 8 | -------------------------------------------------------------------------------- /frontend/src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /frontend/src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /frontend/src/components/ui/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { ChevronRight, MoreHorizontal } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Breadcrumb = React.forwardRef< 8 | HTMLElement, 9 | React.ComponentPropsWithoutRef<"nav"> & { 10 | separator?: React.ReactNode 11 | } 12 | >(({ ...props }, ref) =>