From 59748af8139bd29829599d9fdf623cc031ecb635 Mon Sep 17 00:00:00 2001 From: SoSweetHam Date: Tue, 14 Oct 2025 12:19:36 +0530 Subject: [PATCH 1/3] fix: marketplace --- platforms/marketplace/client/src/App.tsx | 3 +- .../marketplace/client/src/hooks/use-auth.tsx | 100 +------ .../client/src/lib/protected-route.tsx | 33 --- .../client/src/pages/admin-dashboard.tsx | 13 +- .../client/src/pages/app-detail.tsx | 6 +- .../client/src/pages/auth-page.tsx | 114 +------- .../client/src/pages/home-page.tsx | 90 +++--- platforms/marketplace/drizzle.config.ts | 14 - platforms/marketplace/package.json | 30 +- platforms/marketplace/server/auth.ts | 139 --------- platforms/marketplace/server/db.ts | 15 - platforms/marketplace/server/index.ts | 41 +-- platforms/marketplace/server/objectAcl.ts | 123 -------- platforms/marketplace/server/objectStorage.ts | 273 ------------------ platforms/marketplace/server/routes.ts | 168 +---------- platforms/marketplace/server/storage.ts | 139 --------- platforms/marketplace/server/vite.ts | 3 +- platforms/marketplace/shared/schema.ts | 79 ----- platforms/marketplace/tsconfig.json | 5 +- platforms/marketplace/vite.config.ts | 18 +- 20 files changed, 92 insertions(+), 1314 deletions(-) delete mode 100644 platforms/marketplace/client/src/lib/protected-route.tsx delete mode 100644 platforms/marketplace/drizzle.config.ts delete mode 100644 platforms/marketplace/server/auth.ts delete mode 100644 platforms/marketplace/server/db.ts delete mode 100644 platforms/marketplace/server/objectAcl.ts delete mode 100644 platforms/marketplace/server/objectStorage.ts delete mode 100644 platforms/marketplace/server/storage.ts delete mode 100644 platforms/marketplace/shared/schema.ts diff --git a/platforms/marketplace/client/src/App.tsx b/platforms/marketplace/client/src/App.tsx index 9b098bbd..0251df85 100644 --- a/platforms/marketplace/client/src/App.tsx +++ b/platforms/marketplace/client/src/App.tsx @@ -4,7 +4,6 @@ import { QueryClientProvider } from "@tanstack/react-query"; import { Toaster } from "@/components/ui/toaster"; import { TooltipProvider } from "@/components/ui/tooltip"; import { AuthProvider } from "@/hooks/use-auth"; -import { ProtectedRoute } from "@/lib/protected-route"; import HomePage from "@/pages/home-page"; import AppDetailPage from "@/pages/app-detail"; import AdminDashboard from "@/pages/admin-dashboard"; @@ -16,7 +15,7 @@ function Router() { - + diff --git a/platforms/marketplace/client/src/hooks/use-auth.tsx b/platforms/marketplace/client/src/hooks/use-auth.tsx index ce4ee206..0a7f7a2a 100644 --- a/platforms/marketplace/client/src/hooks/use-auth.tsx +++ b/platforms/marketplace/client/src/hooks/use-auth.tsx @@ -1,111 +1,21 @@ import { createContext, ReactNode, useContext } from "react"; -import { - useQuery, - useMutation, - UseMutationResult, -} from "@tanstack/react-query"; -import { insertUserSchema, User as SelectUser, InsertUser } from "@shared/schema"; -import { getQueryFn, apiRequest, queryClient } from "../lib/queryClient"; -import { useToast } from "@/hooks/use-toast"; +// Simplified auth context - no actual authentication type AuthContextType = { - user: SelectUser | null; + user: null; isLoading: boolean; error: Error | null; - loginMutation: UseMutationResult; - logoutMutation: UseMutationResult; - registerMutation: UseMutationResult; -}; - -type LoginData = { - email: string; - password: string; }; export const AuthContext = createContext(null); export function AuthProvider({ children }: { children: ReactNode }) { - const { toast } = useToast(); - const { - data: user, - error, - isLoading, - } = useQuery({ - queryKey: ["/api/user"], - queryFn: getQueryFn({ on401: "returnNull" }), - }); - - const loginMutation = useMutation({ - mutationFn: async (credentials: LoginData) => { - const res = await apiRequest("POST", "/api/login", credentials); - return await res.json(); - }, - onSuccess: (user: SelectUser) => { - queryClient.setQueryData(["/api/user"], user); - toast({ - title: "Welcome back!", - description: "You have successfully signed in.", - }); - }, - onError: (error: Error) => { - toast({ - title: "Login failed", - description: error.message, - variant: "destructive", - }); - }, - }); - - const registerMutation = useMutation({ - mutationFn: async (credentials: InsertUser) => { - const res = await apiRequest("POST", "/api/register", credentials); - return await res.json(); - }, - onSuccess: (user: SelectUser) => { - queryClient.setQueryData(["/api/user"], user); - toast({ - title: "Account created!", - description: "Welcome to the marketplace admin panel.", - }); - }, - onError: (error: Error) => { - toast({ - title: "Registration failed", - description: error.message, - variant: "destructive", - }); - }, - }); - - const logoutMutation = useMutation({ - mutationFn: async () => { - await apiRequest("POST", "/api/logout"); - }, - onSuccess: () => { - queryClient.setQueryData(["/api/user"], null); - toast({ - title: "Signed out", - description: "You have been successfully signed out.", - }); - }, - onError: (error: Error) => { - toast({ - title: "Logout failed", - description: error.message, - variant: "destructive", - }); - }, - }); - return ( {children} diff --git a/platforms/marketplace/client/src/lib/protected-route.tsx b/platforms/marketplace/client/src/lib/protected-route.tsx deleted file mode 100644 index e5324238..00000000 --- a/platforms/marketplace/client/src/lib/protected-route.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { useAuth } from "@/hooks/use-auth"; -import { Loader2 } from "lucide-react"; -import { Redirect, Route } from "wouter"; - -export function ProtectedRoute({ - path, - component: Component, -}: { - path: string; - component: () => React.JSX.Element; -}) { - const { user, isLoading } = useAuth(); - - if (isLoading) { - return ( - -
- -
-
- ); - } - - if (!user?.isAdmin) { - return ( - - - - ); - } - - return ; -} diff --git a/platforms/marketplace/client/src/pages/admin-dashboard.tsx b/platforms/marketplace/client/src/pages/admin-dashboard.tsx index b92961a8..638d521c 100644 --- a/platforms/marketplace/client/src/pages/admin-dashboard.tsx +++ b/platforms/marketplace/client/src/pages/admin-dashboard.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { useQuery, useMutation } from "@tanstack/react-query"; import { Link } from "wouter"; -import { App, InsertApp } from "@shared/schema"; +import { App, InsertApp } from "@/types"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -19,7 +19,7 @@ import { useToast } from "@/hooks/use-toast"; import { Plus, Settings, ExternalLink, LogOut, Edit, Trash2, Star, Eye, TrendingUp, BarChart3 } from "lucide-react"; export default function AdminDashboard() { - const { user, logoutMutation } = useAuth(); + const { user } = useAuth(); const { toast } = useToast(); const [showAddModal, setShowAddModal] = useState(false); const [editingApp, setEditingApp] = useState(null); @@ -274,14 +274,7 @@ export default function AdminDashboard() {
- {user?.email} - + Admin
diff --git a/platforms/marketplace/client/src/pages/app-detail.tsx b/platforms/marketplace/client/src/pages/app-detail.tsx index 32e1798f..a38999cd 100644 --- a/platforms/marketplace/client/src/pages/app-detail.tsx +++ b/platforms/marketplace/client/src/pages/app-detail.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { useQuery, useMutation } from "@tanstack/react-query"; import { useRoute, Link } from "wouter"; -import { App, Review } from "@shared/schema"; +import { App, Review } from "@/types"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; @@ -341,11 +341,11 @@ export default function AppDetailPage() {
- {review.username.substring(0, 2).toUpperCase()} + {review.userName.substring(0, 2).toUpperCase()}
- {review.username} + {review.userName} {renderStars(review.rating)} {new Date(review.createdAt).toLocaleDateString()} diff --git a/platforms/marketplace/client/src/pages/auth-page.tsx b/platforms/marketplace/client/src/pages/auth-page.tsx index 15ca1d52..0f942488 100644 --- a/platforms/marketplace/client/src/pages/auth-page.tsx +++ b/platforms/marketplace/client/src/pages/auth-page.tsx @@ -1,113 +1,29 @@ -import { useState } from "react"; -import { useAuth } from "@/hooks/use-auth"; -import { Redirect } from "wouter"; +import { Link } from "wouter"; import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Checkbox } from "@/components/ui/checkbox"; -import { Shield, Eye, EyeOff } from "lucide-react"; +import { Shield } from "lucide-react"; export default function AuthPage() { - const { user, loginMutation } = useAuth(); - const [showPassword, setShowPassword] = useState(false); - const [loginForm, setLoginForm] = useState({ - email: "", - password: "", - rememberMe: false, - }); - - // Redirect if already logged in - if (user) { - return ; - } - - const handleLogin = (e: React.FormEvent) => { - e.preventDefault(); - loginMutation.mutate({ - email: loginForm.email, - password: loginForm.password, - }); - }; - return (
-
+
-

Admin Access

-

Manage your W3DS marketplace

-
- -
-
- - setLoginForm(prev => ({ ...prev, email: e.target.value }))} - placeholder="admin@example.com" - className="mt-2 rounded-xl border-2 border-gray-200 px-4 py-3" - required - /> -
- -
- -
- setLoginForm(prev => ({ ...prev, password: e.target.value }))} - placeholder="Enter your password" - className="rounded-xl border-2 border-gray-200 px-4 py-3 pr-12" - required - /> - -
-
- -
-
- - setLoginForm(prev => ({ ...prev, rememberMe: checked as boolean })) - } - /> - -
- -
- - -
+ +
diff --git a/platforms/marketplace/client/src/pages/home-page.tsx b/platforms/marketplace/client/src/pages/home-page.tsx index 4db7ac67..0c47e286 100644 --- a/platforms/marketplace/client/src/pages/home-page.tsx +++ b/platforms/marketplace/client/src/pages/home-page.tsx @@ -1,23 +1,19 @@ import { useState } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { Link } from "wouter"; -import { App, Review } from "@shared/schema"; -import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; -import { Star, Search, Store, ArrowRight, ExternalLink } from "lucide-react"; +import { Search, Store, ArrowRight } from "lucide-react"; import w3dsLogo from "@assets/w3dslogo.svg"; +import appsData from "@/data/apps.json"; export default function HomePage() { const [searchQuery, setSearchQuery] = useState(""); const [selectedCategory, setSelectedCategory] = useState("All Apps"); - const { data: apps = [], isLoading: isLoadingApps } = useQuery({ - queryKey: ["/api/apps"], - }); + const apps = appsData; + const isLoadingApps = false; - const categories = ["All Apps", "Productivity", "Design", "Development", "Communication", "Marketing"]; + const categories = ["All Apps", "Identity", "Social", "Governance", "Wellness"]; const filteredApps = apps.filter(app => { const matchesSearch = app.name.toLowerCase().includes(searchQuery.toLowerCase()) || @@ -27,28 +23,6 @@ export default function HomePage() { return matchesSearch && matchesCategory; }); - const renderStars = (rating: number) => { - const fullStars = Math.floor(rating); - const hasHalfStar = rating % 1 >= 0.5; - const emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0); - - return ( -
- {[...Array(fullStars)].map((_, i) => ( - - ))} - {hasHalfStar && ( - - )} - {[...Array(emptyStars)].map((_, i) => ( - - ))} -
- ); - }; - return (
{/* Header */} @@ -155,27 +129,51 @@ export default function HomePage() {

{app.name}

-

{app.category}

-
- {renderStars(parseFloat(app.averageRating || "0"))} - - {parseFloat(app.averageRating || "0").toFixed(1)} - +
+ + {app.category} +

{app.description}

-
- - {app.totalReviews} reviews - - - - +
+ {(app as any).appStoreUrl && (app as any).playStoreUrl ? ( + <> + + + + + + + + ) : ( + + + + )}
))} diff --git a/platforms/marketplace/drizzle.config.ts b/platforms/marketplace/drizzle.config.ts deleted file mode 100644 index 4cd7b4cb..00000000 --- a/platforms/marketplace/drizzle.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { defineConfig } from "drizzle-kit"; - -if (!process.env.DATABASE_URL) { - throw new Error("DATABASE_URL, ensure the database is provisioned"); -} - -export default defineConfig({ - out: "./migrations", - schema: "./shared/schema.ts", - dialect: "postgresql", - dbCredentials: { - url: process.env.DATABASE_URL, - }, -}); diff --git a/platforms/marketplace/package.json b/platforms/marketplace/package.json index 842f9f4c..af37546a 100644 --- a/platforms/marketplace/package.json +++ b/platforms/marketplace/package.json @@ -7,14 +7,11 @@ "dev": "NODE_ENV=development tsx server/index.ts", "build": "vite build && esbuild server/index.ts --platform=node --packages=external --bundle --format=esm --outdir=dist", "start": "NODE_ENV=production node dist/index.js", - "check": "tsc", - "db:push": "drizzle-kit push" + "check": "tsc" }, "dependencies": { - "@google-cloud/storage": "^7.16.0", "@hookform/resolvers": "^3.10.0", "@jridgewell/trace-mapping": "^0.3.25", - "@neondatabase/serverless": "^0.10.4", "@radix-ui/react-accordion": "^1.2.4", "@radix-ui/react-alert-dialog": "^1.1.7", "@radix-ui/react-aspect-ratio": "^1.1.3", @@ -43,31 +40,16 @@ "@radix-ui/react-toggle-group": "^1.1.3", "@radix-ui/react-tooltip": "^1.2.0", "@tanstack/react-query": "^5.60.5", - "@uppy/aws-s3": "^4.3.2", - "@uppy/core": "^4.5.2", - "@uppy/dashboard": "^4.4.3", - "@uppy/drag-drop": "^4.2.2", - "@uppy/file-input": "^4.2.2", - "@uppy/progress-bar": "^4.3.2", - "@uppy/react": "^4.5.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", - "connect-pg-simple": "^10.0.0", "date-fns": "^3.6.0", - "drizzle-orm": "^0.39.1", - "drizzle-zod": "^0.7.0", "embla-carousel-react": "^8.6.0", "express": "^4.21.2", - "express-session": "^1.18.1", "framer-motion": "^11.13.1", - "google-auth-library": "^10.2.1", "input-otp": "^1.4.2", "lucide-react": "^0.453.0", - "memorystore": "^1.6.7", "next-themes": "^0.4.6", - "passport": "^0.7.0", - "passport-local": "^1.0.0", "react": "^18.3.1", "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", @@ -80,7 +62,6 @@ "tw-animate-css": "^1.2.5", "vaul": "^1.1.2", "wouter": "^3.3.5", - "ws": "^8.18.0", "zod": "^3.24.2", "zod-validation-error": "^3.4.0" }, @@ -89,26 +70,17 @@ "@replit/vite-plugin-runtime-error-modal": "^0.0.3", "@tailwindcss/typography": "^0.5.15", "@tailwindcss/vite": "^4.1.3", - "@types/connect-pg-simple": "^7.0.3", "@types/express": "4.17.21", - "@types/express-session": "^1.18.0", "@types/node": "20.16.11", - "@types/passport": "^1.0.16", - "@types/passport-local": "^1.0.38", "@types/react": "^18.3.11", "@types/react-dom": "^18.3.1", - "@types/ws": "^8.5.13", "@vitejs/plugin-react": "^4.3.2", "autoprefixer": "^10.4.20", - "drizzle-kit": "^0.30.4", "esbuild": "^0.25.0", "postcss": "^8.4.47", "tailwindcss": "^3.4.17", "tsx": "^4.19.1", "typescript": "5.6.3", "vite": "^5.4.19" - }, - "optionalDependencies": { - "bufferutil": "^4.0.8" } } \ No newline at end of file diff --git a/platforms/marketplace/server/auth.ts b/platforms/marketplace/server/auth.ts deleted file mode 100644 index bd9b2333..00000000 --- a/platforms/marketplace/server/auth.ts +++ /dev/null @@ -1,139 +0,0 @@ -import passport from "passport"; -import { Strategy as LocalStrategy } from "passport-local"; -import { Express } from "express"; -import session from "express-session"; -import { scrypt, randomBytes, timingSafeEqual } from "crypto"; -import { promisify } from "util"; -import { storage } from "./storage"; -import { User as SelectUser } from "@shared/schema"; - -declare global { - namespace Express { - interface User extends SelectUser {} - } -} - -const scryptAsync = promisify(scrypt); - -async function hashPassword(password: string) { - const salt = randomBytes(16).toString("hex"); - const buf = (await scryptAsync(password, salt, 64)) as Buffer; - return `${buf.toString("hex")}.${salt}`; -} - -async function comparePasswords(supplied: string, stored: string) { - const [hashed, salt] = stored.split("."); - const hashedBuf = Buffer.from(hashed, "hex"); - const suppliedBuf = (await scryptAsync(supplied, salt, 64)) as Buffer; - return timingSafeEqual(hashedBuf, suppliedBuf); -} - -export function setupAuth(app: Express) { - const sessionSettings: session.SessionOptions = { - secret: process.env.SESSION_SECRET || "marketplace-secret-key", - resave: false, - saveUninitialized: false, - store: storage.sessionStore, - cookie: { - secure: process.env.NODE_ENV === "production", - httpOnly: true, - maxAge: 24 * 60 * 60 * 1000, // 24 hours - }, - }; - - app.set("trust proxy", 1); - app.use(session(sessionSettings)); - app.use(passport.initialize()); - app.use(passport.session()); - - passport.use( - new LocalStrategy({ usernameField: "email" }, async (email, password, done) => { - const user = await storage.getUserByEmail(email); - if (!user || !(await comparePasswords(password, user.password))) { - return done(null, false); - } else { - return done(null, user); - } - }), - ); - - passport.serializeUser((user, done) => done(null, user.id)); - passport.deserializeUser(async (id: string, done) => { - const user = await storage.getUser(id); - done(null, user); - }); - - app.post("/api/register", async (req, res, next) => { - try { - const { email, username, password } = req.body; - - const existingUser = await storage.getUserByEmail(email); - if (existingUser) { - return res.status(400).json({ message: "Email already exists" }); - } - - const existingUsername = await storage.getUserByUsername(username); - if (existingUsername) { - return res.status(400).json({ message: "Username already exists" }); - } - - const user = await storage.createUser({ - email, - username, - password: await hashPassword(password), - }); - - req.login(user, (err) => { - if (err) return next(err); - res.status(201).json({ - id: user.id, - email: user.email, - username: user.username, - isAdmin: user.isAdmin - }); - }); - } catch (error) { - next(error); - } - }); - - app.post("/api/login", passport.authenticate("local"), (req, res) => { - const user = req.user!; - res.status(200).json({ - id: user.id, - email: user.email, - username: user.username, - isAdmin: user.isAdmin - }); - }); - - app.post("/api/logout", (req, res, next) => { - req.logout((err) => { - if (err) return next(err); - res.sendStatus(200); - }); - }); - - app.get("/api/user", (req, res) => { - if (!req.isAuthenticated()) return res.sendStatus(401); - const user = req.user!; - res.json({ - id: user.id, - email: user.email, - username: user.username, - isAdmin: user.isAdmin - }); - }); -} - -export function requireAdmin(req: any, res: any, next: any) { - if (!req.isAuthenticated()) { - return res.status(401).json({ message: "Authentication required" }); - } - - if (!req.user.isAdmin) { - return res.status(403).json({ message: "Admin access required" }); - } - - next(); -} diff --git a/platforms/marketplace/server/db.ts b/platforms/marketplace/server/db.ts deleted file mode 100644 index 66779a9d..00000000 --- a/platforms/marketplace/server/db.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Pool, neonConfig } from '@neondatabase/serverless'; -import { drizzle } from 'drizzle-orm/neon-serverless'; -import ws from "ws"; -import * as schema from "@shared/schema"; - -neonConfig.webSocketConstructor = ws; - -if (!process.env.DATABASE_URL) { - throw new Error( - "DATABASE_URL must be set. Did you forget to provision a database?", - ); -} - -export const pool = new Pool({ connectionString: process.env.DATABASE_URL }); -export const db = drizzle({ client: pool, schema }); diff --git a/platforms/marketplace/server/index.ts b/platforms/marketplace/server/index.ts index 8bf19122..a06907be 100644 --- a/platforms/marketplace/server/index.ts +++ b/platforms/marketplace/server/index.ts @@ -1,38 +1,24 @@ import express, { type Request, Response, NextFunction } from "express"; import { registerRoutes } from "./routes"; import { setupVite, serveStatic, log } from "./vite"; +import path from "path"; const app = express(); app.use(express.json()); app.use(express.urlencoded({ extended: false })); +// Serve static assets +app.use('/assets', express.static(path.resolve(import.meta.dirname, '..', 'assets'))); + +// Simple logging middleware app.use((req, res, next) => { const start = Date.now(); - const path = req.path; - let capturedJsonResponse: Record | undefined = undefined; - - const originalResJson = res.json; - res.json = function (bodyJson, ...args) { - capturedJsonResponse = bodyJson; - return originalResJson.apply(res, [bodyJson, ...args]); - }; - res.on("finish", () => { const duration = Date.now() - start; - if (path.startsWith("/api")) { - let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`; - if (capturedJsonResponse) { - logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`; - } - - if (logLine.length > 80) { - logLine = logLine.slice(0, 79) + "…"; - } - - log(logLine); + if (req.path.startsWith("/api")) { + log(`${req.method} ${req.path} ${res.statusCode} in ${duration}ms`); } }); - next(); }); @@ -42,30 +28,23 @@ app.use((req, res, next) => { app.use((err: any, _req: Request, res: Response, _next: NextFunction) => { const status = err.status || err.statusCode || 500; const message = err.message || "Internal Server Error"; - res.status(status).json({ message }); - throw err; + console.error(err); }); - // importantly only setup vite in development and after - // setting up all the other routes so the catch-all route - // doesn't interfere with the other routes + // Setup vite in development and static serving in production if (app.get("env") === "development") { await setupVite(app, server); } else { serveStatic(app); } - // ALWAYS serve the app on the port specified in the environment variable PORT - // Other ports are firewalled. Default to 5000 if not specified. - // this serves both the API and the client. - // It is the only port that is not firewalled. const port = parseInt(process.env.PORT || '5000', 10); server.listen({ port, host: "0.0.0.0", reusePort: true, }, () => { - log(`serving on port ${port}`); + log(`Marketplace UI server running on port ${port}`); }); })(); diff --git a/platforms/marketplace/server/objectAcl.ts b/platforms/marketplace/server/objectAcl.ts deleted file mode 100644 index 297ce779..00000000 --- a/platforms/marketplace/server/objectAcl.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { File } from "@google-cloud/storage"; - -const ACL_POLICY_METADATA_KEY = "custom:aclPolicy"; - -export enum ObjectAccessGroupType {} - -export interface ObjectAccessGroup { - type: ObjectAccessGroupType; - id: string; -} - -export enum ObjectPermission { - READ = "read", - WRITE = "write", -} - -export interface ObjectAclRule { - group: ObjectAccessGroup; - permission: ObjectPermission; -} - -export interface ObjectAclPolicy { - owner: string; - visibility: "public" | "private"; - aclRules?: Array; -} - -function isPermissionAllowed( - requested: ObjectPermission, - granted: ObjectPermission, -): boolean { - if (requested === ObjectPermission.READ) { - return [ObjectPermission.READ, ObjectPermission.WRITE].includes(granted); - } - return granted === ObjectPermission.WRITE; -} - -abstract class BaseObjectAccessGroup implements ObjectAccessGroup { - constructor( - public readonly type: ObjectAccessGroupType, - public readonly id: string, - ) {} - - public abstract hasMember(userId: string): Promise; -} - -function createObjectAccessGroup( - group: ObjectAccessGroup, -): BaseObjectAccessGroup { - switch (group.type) { - default: - throw new Error(`Unknown access group type: ${group.type}`); - } -} - -export async function setObjectAclPolicy( - objectFile: File, - aclPolicy: ObjectAclPolicy, -): Promise { - const [exists] = await objectFile.exists(); - if (!exists) { - throw new Error(`Object not found: ${objectFile.name}`); - } - - await objectFile.setMetadata({ - metadata: { - [ACL_POLICY_METADATA_KEY]: JSON.stringify(aclPolicy), - }, - }); -} - -export async function getObjectAclPolicy( - objectFile: File, -): Promise { - const [metadata] = await objectFile.getMetadata(); - const aclPolicy = metadata?.metadata?.[ACL_POLICY_METADATA_KEY]; - if (!aclPolicy) { - return null; - } - return JSON.parse(aclPolicy as string); -} - -export async function canAccessObject({ - userId, - objectFile, - requestedPermission, -}: { - userId?: string; - objectFile: File; - requestedPermission: ObjectPermission; -}): Promise { - const aclPolicy = await getObjectAclPolicy(objectFile); - if (!aclPolicy) { - return false; - } - - if ( - aclPolicy.visibility === "public" && - requestedPermission === ObjectPermission.READ - ) { - return true; - } - - if (!userId) { - return false; - } - - if (aclPolicy.owner === userId) { - return true; - } - - for (const rule of aclPolicy.aclRules || []) { - const accessGroup = createObjectAccessGroup(rule.group); - if ( - (await accessGroup.hasMember(userId)) && - isPermissionAllowed(requestedPermission, rule.permission) - ) { - return true; - } - } - - return false; -} diff --git a/platforms/marketplace/server/objectStorage.ts b/platforms/marketplace/server/objectStorage.ts deleted file mode 100644 index 58620f63..00000000 --- a/platforms/marketplace/server/objectStorage.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { Storage, File } from "@google-cloud/storage"; -import { Response } from "express"; -import { randomUUID } from "crypto"; -import { - ObjectAclPolicy, - ObjectPermission, - canAccessObject, - getObjectAclPolicy, - setObjectAclPolicy, -} from "./objectAcl"; - -const REPLIT_SIDECAR_ENDPOINT = "http://127.0.0.1:1106"; - -export const objectStorageClient = new Storage({ - credentials: { - audience: "replit", - subject_token_type: "access_token", - token_url: `${REPLIT_SIDECAR_ENDPOINT}/token`, - type: "external_account", - credential_source: { - url: `${REPLIT_SIDECAR_ENDPOINT}/credential`, - format: { - type: "json", - subject_token_field_name: "access_token", - }, - }, - universe_domain: "googleapis.com", - }, - projectId: "", -}); - -export class ObjectNotFoundError extends Error { - constructor() { - super("Object not found"); - this.name = "ObjectNotFoundError"; - Object.setPrototypeOf(this, ObjectNotFoundError.prototype); - } -} - -export class ObjectStorageService { - constructor() {} - - getPublicObjectSearchPaths(): Array { - const pathsStr = process.env.PUBLIC_OBJECT_SEARCH_PATHS || ""; - const paths = Array.from( - new Set( - pathsStr - .split(",") - .map((path) => path.trim()) - .filter((path) => path.length > 0) - ) - ); - if (paths.length === 0) { - throw new Error( - "PUBLIC_OBJECT_SEARCH_PATHS not set. Create a bucket in 'Object Storage' " + - "tool and set PUBLIC_OBJECT_SEARCH_PATHS env var (comma-separated paths)." - ); - } - return paths; - } - - getPrivateObjectDir(): string { - const dir = process.env.PRIVATE_OBJECT_DIR || ""; - if (!dir) { - throw new Error( - "PRIVATE_OBJECT_DIR not set. Create a bucket in 'Object Storage' " + - "tool and set PRIVATE_OBJECT_DIR env var." - ); - } - return dir; - } - - async searchPublicObject(filePath: string): Promise { - for (const searchPath of this.getPublicObjectSearchPaths()) { - const fullPath = `${searchPath}/${filePath}`; - const { bucketName, objectName } = parseObjectPath(fullPath); - const bucket = objectStorageClient.bucket(bucketName); - const file = bucket.file(objectName); - - const [exists] = await file.exists(); - if (exists) { - return file; - } - } - return null; - } - - async downloadObject(file: File, res: Response, cacheTtlSec: number = 3600) { - try { - const [metadata] = await file.getMetadata(); - const aclPolicy = await getObjectAclPolicy(file); - const isPublic = aclPolicy?.visibility === "public"; - - res.set({ - "Content-Type": metadata.contentType || "application/octet-stream", - "Content-Length": metadata.size, - "Cache-Control": `${isPublic ? "public" : "private"}, max-age=${cacheTtlSec}`, - }); - - const stream = file.createReadStream(); - stream.on("error", (err) => { - console.error("Stream error:", err); - if (!res.headersSent) { - res.status(500).json({ error: "Error streaming file" }); - } - }); - - stream.pipe(res); - } catch (error) { - console.error("Error downloading file:", error); - if (!res.headersSent) { - res.status(500).json({ error: "Error downloading file" }); - } - } - } - - async getObjectEntityUploadURL(): Promise { - const privateObjectDir = this.getPrivateObjectDir(); - if (!privateObjectDir) { - throw new Error( - "PRIVATE_OBJECT_DIR not set. Create a bucket in 'Object Storage' " + - "tool and set PRIVATE_OBJECT_DIR env var." - ); - } - - const objectId = randomUUID(); - const fullPath = `${privateObjectDir}/uploads/${objectId}`; - const { bucketName, objectName } = parseObjectPath(fullPath); - - return signObjectURL({ - bucketName, - objectName, - method: "PUT", - ttlSec: 900, - }); - } - - async getObjectEntityFile(objectPath: string): Promise { - if (!objectPath.startsWith("/objects/")) { - throw new ObjectNotFoundError(); - } - - const parts = objectPath.slice(1).split("/"); - if (parts.length < 2) { - throw new ObjectNotFoundError(); - } - - const entityId = parts.slice(1).join("/"); - let entityDir = this.getPrivateObjectDir(); - if (!entityDir.endsWith("/")) { - entityDir = `${entityDir}/`; - } - const objectEntityPath = `${entityDir}${entityId}`; - const { bucketName, objectName } = parseObjectPath(objectEntityPath); - const bucket = objectStorageClient.bucket(bucketName); - const objectFile = bucket.file(objectName); - const [exists] = await objectFile.exists(); - if (!exists) { - throw new ObjectNotFoundError(); - } - return objectFile; - } - - normalizeObjectEntityPath(rawPath: string): string { - if (!rawPath.startsWith("https://storage.googleapis.com/")) { - return rawPath; - } - - const url = new URL(rawPath); - const rawObjectPath = url.pathname; - - let objectEntityDir = this.getPrivateObjectDir(); - if (!objectEntityDir.endsWith("/")) { - objectEntityDir = `${objectEntityDir}/`; - } - - if (!rawObjectPath.startsWith(objectEntityDir)) { - return rawObjectPath; - } - - const entityId = rawObjectPath.slice(objectEntityDir.length); - return `/objects/${entityId}`; - } - - async trySetObjectEntityAclPolicy( - rawPath: string, - aclPolicy: ObjectAclPolicy - ): Promise { - const normalizedPath = this.normalizeObjectEntityPath(rawPath); - if (!normalizedPath.startsWith("/")) { - return normalizedPath; - } - - const objectFile = await this.getObjectEntityFile(normalizedPath); - await setObjectAclPolicy(objectFile, aclPolicy); - return normalizedPath; - } - - async canAccessObjectEntity({ - userId, - objectFile, - requestedPermission, - }: { - userId?: string; - objectFile: File; - requestedPermission?: ObjectPermission; - }): Promise { - return canAccessObject({ - userId, - objectFile, - requestedPermission: requestedPermission ?? ObjectPermission.READ, - }); - } -} - -function parseObjectPath(path: string): { - bucketName: string; - objectName: string; -} { - if (!path.startsWith("/")) { - path = `/${path}`; - } - const pathParts = path.split("/"); - if (pathParts.length < 3) { - throw new Error("Invalid path: must contain at least a bucket name"); - } - - const bucketName = pathParts[1]; - const objectName = pathParts.slice(2).join("/"); - - return { - bucketName, - objectName, - }; -} - -async function signObjectURL({ - bucketName, - objectName, - method, - ttlSec, -}: { - bucketName: string; - objectName: string; - method: "GET" | "PUT" | "DELETE" | "HEAD"; - ttlSec: number; -}): Promise { - const request = { - bucket_name: bucketName, - object_name: objectName, - method, - expires_at: new Date(Date.now() + ttlSec * 1000).toISOString(), - }; - const response = await fetch( - `${REPLIT_SIDECAR_ENDPOINT}/object-storage/signed-object-url`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(request), - } - ); - if (!response.ok) { - throw new Error( - `Failed to sign object URL, errorcode: ${response.status}, ` + - `make sure you're running on Replit` - ); - } - - const { signed_url: signedURL } = await response.json(); - return signedURL; -} diff --git a/platforms/marketplace/server/routes.ts b/platforms/marketplace/server/routes.ts index 6d88646c..07513229 100644 --- a/platforms/marketplace/server/routes.ts +++ b/platforms/marketplace/server/routes.ts @@ -1,172 +1,10 @@ import type { Express } from "express"; import { createServer, type Server } from "http"; -import { setupAuth, requireAdmin } from "./auth"; -import { storage } from "./storage"; -import { insertAppSchema, insertReviewSchema } from "@shared/schema"; -import { ObjectStorageService, ObjectNotFoundError } from "./objectStorage"; export async function registerRoutes(app: Express): Promise { - setupAuth(app); - - const objectStorageService = new ObjectStorageService(); - - // Public app routes - app.get("/api/apps", async (req, res) => { - try { - const apps = await storage.getApps(); - res.json(apps.filter(app => app.status === "active")); - } catch (error) { - console.error("Error fetching apps:", error); - res.status(500).json({ message: "Failed to fetch apps" }); - } - }); - - app.get("/api/apps/:id", async (req, res) => { - try { - const app = await storage.getApp(req.params.id); - if (!app) { - return res.status(404).json({ message: "App not found" }); - } - res.json(app); - } catch (error) { - console.error("Error fetching app:", error); - res.status(500).json({ message: "Failed to fetch app" }); - } - }); - - app.get("/api/apps/:id/reviews", async (req, res) => { - try { - const reviews = await storage.getReviews(req.params.id); - res.json(reviews); - } catch (error) { - console.error("Error fetching reviews:", error); - res.status(500).json({ message: "Failed to fetch reviews" }); - } - }); - - app.post("/api/apps/:id/reviews", async (req, res) => { - try { - const reviewData = insertReviewSchema.parse({ - ...req.body, - appId: req.params.id, - }); - - const review = await storage.createReview(reviewData); - res.status(201).json(review); - } catch (error) { - console.error("Error creating review:", error); - res.status(400).json({ message: "Invalid review data" }); - } - }); - - // Admin app management routes - app.get("/api/admin/apps", requireAdmin, async (req, res) => { - try { - const apps = await storage.getApps(); - res.json(apps); - } catch (error) { - console.error("Error fetching admin apps:", error); - res.status(500).json({ message: "Failed to fetch apps" }); - } - }); - - app.post("/api/admin/apps", requireAdmin, async (req, res) => { - try { - const appData = insertAppSchema.parse(req.body); - const app = await storage.createApp(appData); - res.status(201).json(app); - } catch (error) { - console.error("Error creating app:", error); - res.status(400).json({ message: "Invalid app data" }); - } - }); - - app.put("/api/admin/apps/:id", requireAdmin, async (req, res) => { - try { - const appData = insertAppSchema.partial().parse(req.body); - const app = await storage.updateApp(req.params.id, appData); - if (!app) { - return res.status(404).json({ message: "App not found" }); - } - res.json(app); - } catch (error) { - console.error("Error updating app:", error); - res.status(400).json({ message: "Invalid app data" }); - } - }); - - app.delete("/api/admin/apps/:id", requireAdmin, async (req, res) => { - try { - const deleted = await storage.deleteApp(req.params.id); - if (!deleted) { - return res.status(404).json({ message: "App not found" }); - } - res.status(204).send(); - } catch (error) { - console.error("Error deleting app:", error); - res.status(500).json({ message: "Failed to delete app" }); - } - }); - - // Object storage routes - app.get("/public-objects/:filePath(*)", async (req, res) => { - const filePath = req.params.filePath; - try { - const file = await objectStorageService.searchPublicObject(filePath); - if (!file) { - return res.status(404).json({ error: "File not found" }); - } - objectStorageService.downloadObject(file, res); - } catch (error) { - console.error("Error searching for public object:", error); - return res.status(500).json({ error: "Internal server error" }); - } - }); - - app.get("/objects/:objectPath(*)", async (req, res) => { - try { - const objectFile = await objectStorageService.getObjectEntityFile(req.path); - objectStorageService.downloadObject(objectFile, res); - } catch (error) { - console.error("Error accessing object:", error); - if (error instanceof ObjectNotFoundError) { - return res.sendStatus(404); - } - return res.sendStatus(500); - } - }); - - app.post("/api/objects/upload", requireAdmin, async (req, res) => { - try { - const uploadURL = await objectStorageService.getObjectEntityUploadURL(); - res.json({ uploadURL }); - } catch (error) { - console.error("Error getting upload URL:", error); - res.status(500).json({ error: "Failed to get upload URL" }); - } - }); - - app.put("/api/objects/finalize", requireAdmin, async (req, res) => { - if (!req.body.fileURL) { - return res.status(400).json({ error: "fileURL is required" }); - } - - try { - const objectPath = await objectStorageService.trySetObjectEntityAclPolicy( - req.body.fileURL, - { - owner: req.user!.id, - visibility: "public", - }, - ); - - res.status(200).json({ - objectPath: objectPath, - }); - } catch (error) { - console.error("Error finalizing file upload:", error); - res.status(500).json({ error: "Internal server error" }); - } + // Simple health check endpoint + app.get("/api/health", (req, res) => { + res.json({ status: "ok", message: "Marketplace server is running" }); }); const httpServer = createServer(app); diff --git a/platforms/marketplace/server/storage.ts b/platforms/marketplace/server/storage.ts deleted file mode 100644 index a9ef0b3b..00000000 --- a/platforms/marketplace/server/storage.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { users, apps, reviews, type User, type InsertUser, type App, type InsertApp, type Review, type InsertReview } from "@shared/schema"; -import { db } from "./db"; -import { eq, desc, sql } from "drizzle-orm"; -import session from "express-session"; -import createMemoryStore from "memorystore"; - -const MemoryStore = createMemoryStore(session); - -export interface IStorage { - getUser(id: string): Promise; - getUserByEmail(email: string): Promise; - getUserByUsername(username: string): Promise; - createUser(user: InsertUser): Promise; - - getApps(): Promise; - getApp(id: string): Promise; - createApp(app: InsertApp): Promise; - updateApp(id: string, app: Partial): Promise; - deleteApp(id: string): Promise; - - getReviews(appId: string): Promise; - createReview(review: InsertReview): Promise; - updateAppRating(appId: string): Promise; - - sessionStore: session.SessionStore; -} - -export class DatabaseStorage implements IStorage { - sessionStore: session.SessionStore; - - constructor() { - this.sessionStore = new MemoryStore({ - checkPeriod: 86400000, - }); - } - - async getUser(id: string): Promise { - const [user] = await db.select().from(users).where(eq(users.id, id)); - return user || undefined; - } - - async getUserByEmail(email: string): Promise { - const [user] = await db.select().from(users).where(eq(users.email, email)); - return user || undefined; - } - - async getUserByUsername(username: string): Promise { - const [user] = await db.select().from(users).where(eq(users.username, username)); - return user || undefined; - } - - async createUser(insertUser: InsertUser): Promise { - const [user] = await db - .insert(users) - .values({ ...insertUser, isAdmin: true }) // First user is admin - .returning(); - return user; - } - - async getApps(): Promise { - return await db.select().from(apps).orderBy(desc(apps.createdAt)); - } - - async getApp(id: string): Promise { - const [app] = await db.select().from(apps).where(eq(apps.id, id)); - return app || undefined; - } - - async createApp(insertApp: InsertApp): Promise { - const [app] = await db - .insert(apps) - .values({ - ...insertApp, - updatedAt: new Date(), - }) - .returning(); - return app; - } - - async updateApp(id: string, updateData: Partial): Promise { - const [app] = await db - .update(apps) - .set({ - ...updateData, - updatedAt: new Date(), - }) - .where(eq(apps.id, id)) - .returning(); - return app || undefined; - } - - async deleteApp(id: string): Promise { - const result = await db.delete(apps).where(eq(apps.id, id)); - return (result.rowCount || 0) > 0; - } - - async getReviews(appId: string): Promise { - return await db - .select() - .from(reviews) - .where(eq(reviews.appId, appId)) - .orderBy(desc(reviews.createdAt)); - } - - async createReview(insertReview: InsertReview): Promise { - const [review] = await db - .insert(reviews) - .values(insertReview) - .returning(); - - // Update app rating - await this.updateAppRating(insertReview.appId); - - return review; - } - - async updateAppRating(appId: string): Promise { - const result = await db - .select({ - avgRating: sql`COALESCE(ROUND(AVG(${reviews.rating}::numeric), 2), 0)::text`, - count: sql`COUNT(*)`, - }) - .from(reviews) - .where(eq(reviews.appId, appId)); - - const { avgRating, count } = result[0]; - - await db - .update(apps) - .set({ - averageRating: avgRating || "0.00", - totalReviews: count || 0, - updatedAt: new Date(), - }) - .where(eq(apps.id, appId)); - } -} - -export const storage = new DatabaseStorage(); diff --git a/platforms/marketplace/server/vite.ts b/platforms/marketplace/server/vite.ts index 9338c14f..64b9ad1b 100644 --- a/platforms/marketplace/server/vite.ts +++ b/platforms/marketplace/server/vite.ts @@ -4,7 +4,6 @@ import path from "path"; import { createServer as createViteServer, createLogger } from "vite"; import { type Server } from "http"; import viteConfig from "../vite.config"; -import { nanoid } from "nanoid"; const viteLogger = createLogger(); @@ -56,7 +55,7 @@ export async function setupVite(app: Express, server: Server) { let template = await fs.promises.readFile(clientTemplate, "utf-8"); template = template.replace( `src="/src/main.tsx"`, - `src="/src/main.tsx?v=${nanoid()}"`, + `src="/src/main.tsx?v=${Date.now()}"`, ); const page = await vite.transformIndexHtml(url, template); res.status(200).set({ "Content-Type": "text/html" }).end(page); diff --git a/platforms/marketplace/shared/schema.ts b/platforms/marketplace/shared/schema.ts deleted file mode 100644 index 9de0c250..00000000 --- a/platforms/marketplace/shared/schema.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { sql } from "drizzle-orm"; -import { pgTable, text, varchar, integer, decimal, timestamp, boolean } from "drizzle-orm/pg-core"; -import { relations } from "drizzle-orm"; -import { createInsertSchema } from "drizzle-zod"; -import { z } from "zod"; - -export const users = pgTable("users", { - id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), - username: text("username").notNull().unique(), - email: text("email").notNull().unique(), - password: text("password").notNull(), - isAdmin: boolean("is_admin").default(false).notNull(), - createdAt: timestamp("created_at").defaultNow().notNull(), -}); - -export const apps = pgTable("apps", { - id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), - name: text("name").notNull(), - description: text("description").notNull(), - fullDescription: text("full_description"), - category: text("category").notNull(), - link: text("link").notNull(), - logoUrl: text("logo_url"), - screenshots: text("screenshots").array(), - status: text("status").default("active").notNull(), // active, pending, inactive - averageRating: decimal("average_rating", { precision: 3, scale: 2 }).default("0"), - totalReviews: integer("total_reviews").default(0).notNull(), - createdAt: timestamp("created_at").defaultNow().notNull(), - updatedAt: timestamp("updated_at").defaultNow().notNull(), -}); - -export const reviews = pgTable("reviews", { - id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), - appId: varchar("app_id").notNull().references(() => apps.id, { onDelete: "cascade" }), - username: text("username").notNull(), - rating: integer("rating").notNull(), // 1-5 - comment: text("comment"), - createdAt: timestamp("created_at").defaultNow().notNull(), -}); - -// Relations -export const appsRelations = relations(apps, ({ many }) => ({ - reviews: many(reviews), -})); - -export const reviewsRelations = relations(reviews, ({ one }) => ({ - app: one(apps, { - fields: [reviews.appId], - references: [apps.id], - }), -})); - -// Insert schemas -export const insertUserSchema = createInsertSchema(users).pick({ - username: true, - email: true, - password: true, -}); - -export const insertAppSchema = createInsertSchema(apps).omit({ - id: true, - averageRating: true, - totalReviews: true, - createdAt: true, - updatedAt: true, -}); - -export const insertReviewSchema = createInsertSchema(reviews).omit({ - id: true, - createdAt: true, -}); - -// Types -export type InsertUser = z.infer; -export type User = typeof users.$inferSelect; -export type InsertApp = z.infer; -export type App = typeof apps.$inferSelect; -export type InsertReview = z.infer; -export type Review = typeof reviews.$inferSelect; diff --git a/platforms/marketplace/tsconfig.json b/platforms/marketplace/tsconfig.json index a0203eef..5caadb25 100644 --- a/platforms/marketplace/tsconfig.json +++ b/platforms/marketplace/tsconfig.json @@ -1,5 +1,5 @@ { - "include": ["client/src/**/*", "shared/**/*", "server/**/*"], + "include": ["client/src/**/*", "server/**/*"], "exclude": ["node_modules", "build", "dist", "**/*.test.ts"], "compilerOptions": { "incremental": true, @@ -16,8 +16,7 @@ "baseUrl": ".", "types": ["node", "vite/client"], "paths": { - "@/*": ["./client/src/*"], - "@shared/*": ["./shared/*"] + "@/*": ["./client/src/*"] } } } diff --git a/platforms/marketplace/vite.config.ts b/platforms/marketplace/vite.config.ts index ce3e3d1f..de066baf 100644 --- a/platforms/marketplace/vite.config.ts +++ b/platforms/marketplace/vite.config.ts @@ -1,26 +1,15 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import path from "path"; -import runtimeErrorOverlay from "@replit/vite-plugin-runtime-error-modal"; export default defineConfig({ plugins: [ react(), - runtimeErrorOverlay(), - ...(process.env.NODE_ENV !== "production" && - process.env.REPL_ID !== undefined - ? [ - await import("@replit/vite-plugin-cartographer").then((m) => - m.cartographer(), - ), - ] - : []), ], resolve: { alias: { "@": path.resolve(import.meta.dirname, "client", "src"), - "@shared": path.resolve(import.meta.dirname, "shared"), - "@assets": path.resolve(import.meta.dirname, "attached_assets"), + "@assets": path.resolve(import.meta.dirname, "assets"), }, }, root: path.resolve(import.meta.dirname, "client"), @@ -30,8 +19,9 @@ export default defineConfig({ }, server: { fs: { - strict: true, - deny: ["**/.*"], + strict: false, + allow: [".."], }, }, + publicDir: path.resolve(import.meta.dirname, "assets"), }); From 6d1e7f3af740b65da39df052bb0ac20d21621fef Mon Sep 17 00:00:00 2001 From: SoSweetHam Date: Tue, 14 Oct 2025 12:19:48 +0530 Subject: [PATCH 2/3] fix: add missing files --- platforms/marketplace/assets/blabsy.svg | 1 + platforms/marketplace/assets/charter.png | Bin 0 -> 50613 bytes platforms/marketplace/assets/eid-w3ds.png | Bin 0 -> 15876 bytes platforms/marketplace/assets/evoting.png | Bin 0 -> 5768 bytes platforms/marketplace/assets/pictique.svg | 50 +++++++++++++++++ platforms/marketplace/assets/w3dslogo.svg | 5 ++ .../marketplace/client/src/data/apps.json | 51 ++++++++++++++++++ platforms/marketplace/client/src/types.ts | 41 ++++++++++++++ 8 files changed, 148 insertions(+) create mode 100644 platforms/marketplace/assets/blabsy.svg create mode 100644 platforms/marketplace/assets/charter.png create mode 100644 platforms/marketplace/assets/eid-w3ds.png create mode 100644 platforms/marketplace/assets/evoting.png create mode 100644 platforms/marketplace/assets/pictique.svg create mode 100644 platforms/marketplace/assets/w3dslogo.svg create mode 100644 platforms/marketplace/client/src/data/apps.json create mode 100644 platforms/marketplace/client/src/types.ts diff --git a/platforms/marketplace/assets/blabsy.svg b/platforms/marketplace/assets/blabsy.svg new file mode 100644 index 00000000..8579d246 --- /dev/null +++ b/platforms/marketplace/assets/blabsy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/platforms/marketplace/assets/charter.png b/platforms/marketplace/assets/charter.png new file mode 100644 index 0000000000000000000000000000000000000000..25f74f452b2f779dfefecd5e1ab1b9a8a965ef38 GIT binary patch literal 50613 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/platforms/marketplace/assets/w3dslogo.svg b/platforms/marketplace/assets/w3dslogo.svg new file mode 100644 index 00000000..7a0ad26c --- /dev/null +++ b/platforms/marketplace/assets/w3dslogo.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/platforms/marketplace/client/src/data/apps.json b/platforms/marketplace/client/src/data/apps.json new file mode 100644 index 00000000..00fa9ef8 --- /dev/null +++ b/platforms/marketplace/client/src/data/apps.json @@ -0,0 +1,51 @@ +[ + { + "id": "eid-wallet", + "name": "eID for W3DS", + "description": "Secure digital identity wallet for W3DS. Maintain sovereign control over your digital identity.", + "category": "Identity", + "logoUrl": "/eid-w3ds.png", + "appStoreUrl": "https://apps.apple.com/in/app/eid-for-w3ds/id6747748667", + "playStoreUrl": "https://play.google.com/store/apps/details?id=foundation.metastate.eid_wallet" + }, + { + "id": "blabsy", + "name": "Blabsy", + "description": "Micro blogging first style application for sharing thoughts across the W3DS ecosystem.", + "category": "Social", + "logoUrl": "/blabsy.svg", + "url": "https://blabsy.w3ds.metastate.foundation" + }, + { + "id": "pictique", + "name": "Pictique", + "description": "Photo sharing first style application for sharing moments across the W3DS ecosystem.", + "category": "Social", + "logoUrl": "/pictique.svg", + "url": "https://pictique.w3ds.metastate.foundation" + }, + { + "id": "evoting", + "name": "eVoting", + "description": "Secure, transparent, and verifiable electronic voting platform with cryptographic guarantees.", + "category": "Governance", + "logoUrl": "/evoting.png", + "url": "https://evoting.w3ds.metastate.foundation" + }, + { + "id": "group-charter", + "name": "Charter Manager", + "description": "Define rules, manage memberships, and ensure transparent governance for your communities.", + "category": "Governance", + "logoUrl": "/charter.png", + "url": "https://charter.w3ds.metastate.foundation" + }, + { + "id": "dreamsync", + "name": "DreamSync", + "description": "Individual discovery platform, find people of interest across the W3DS ecosystem.", + "category": "Wellness", + "logoUrl": null, + "url": "https://dreamsync.w3ds.metastate.foundation" + } +] diff --git a/platforms/marketplace/client/src/types.ts b/platforms/marketplace/client/src/types.ts new file mode 100644 index 00000000..ec33649e --- /dev/null +++ b/platforms/marketplace/client/src/types.ts @@ -0,0 +1,41 @@ +// Type definitions for the marketplace +export type App = { + id: string; + name: string; + description: string; + fullDescription: string | null; + category: string; + link: string; + logoUrl: string | null; + screenshots: string[] | null; + status: string; + averageRating: string | null; + totalReviews: number; + createdAt: Date; + updatedAt: Date; +}; + +export type InsertApp = Omit; + +export type Review = { + id: string; + appId: string; + userName: string; + rating: number; + comment: string | null; + createdAt: Date; +}; + +export type InsertReview = Omit; + +export type User = { + id: string; + email: string; + username: string; + password: string; + isAdmin: boolean; + createdAt: Date; +}; + +export type InsertUser = Omit; + From 1e738e4c200841b2f1b8f0ff3752cc7a0bec69b2 Mon Sep 17 00:00:00 2001 From: SoSweetHam Date: Tue, 14 Oct 2025 12:33:12 +0530 Subject: [PATCH 3/3] fix: add app details page --- .../client/src/components/ObjectUploader.tsx | 56 +- .../client/src/pages/app-detail.tsx | 344 ++------- .../client/src/pages/home-page.tsx | 116 +-- pnpm-lock.yaml | 690 +----------------- 4 files changed, 164 insertions(+), 1042 deletions(-) diff --git a/platforms/marketplace/client/src/components/ObjectUploader.tsx b/platforms/marketplace/client/src/components/ObjectUploader.tsx index c9617095..511f88bf 100644 --- a/platforms/marketplace/client/src/components/ObjectUploader.tsx +++ b/platforms/marketplace/client/src/components/ObjectUploader.tsx @@ -1,67 +1,29 @@ -import { useState } from "react"; import type { ReactNode } from "react"; -import Uppy from "@uppy/core"; -import { DashboardModal } from "@uppy/react"; -import "@uppy/core/dist/style.min.css"; -import "@uppy/dashboard/dist/style.min.css"; -import AwsS3 from "@uppy/aws-s3"; -import type { UploadResult } from "@uppy/core"; import { Button } from "@/components/ui/button"; interface ObjectUploaderProps { maxNumberOfFiles?: number; maxFileSize?: number; - onGetUploadParameters: () => Promise<{ + onGetUploadParameters?: () => Promise<{ method: "PUT"; url: string; }>; - onComplete?: ( - result: UploadResult, Record> - ) => void; + onComplete?: (result: any) => void; buttonClassName?: string; children: ReactNode; } export function ObjectUploader({ - maxNumberOfFiles = 1, - maxFileSize = 10485760, // 10MB default - onGetUploadParameters, - onComplete, buttonClassName, children, }: ObjectUploaderProps) { - const [showModal, setShowModal] = useState(false); - const [uppy] = useState(() => - new Uppy({ - restrictions: { - maxNumberOfFiles, - maxFileSize, - allowedFileTypes: ['image/*'], - }, - autoProceed: false, - }) - .use(AwsS3, { - shouldUseMultipart: false, - getUploadParameters: onGetUploadParameters, - }) - .on("complete", (result) => { - onComplete?.(result); - setShowModal(false); - }) - ); - return ( -
- - - setShowModal(false)} - proudlyDisplayPoweredByUppy={false} - /> -
+ ); } diff --git a/platforms/marketplace/client/src/pages/app-detail.tsx b/platforms/marketplace/client/src/pages/app-detail.tsx index a38999cd..367c63a3 100644 --- a/platforms/marketplace/client/src/pages/app-detail.tsx +++ b/platforms/marketplace/client/src/pages/app-detail.tsx @@ -1,129 +1,43 @@ -import { useState } from "react"; -import { useQuery, useMutation } from "@tanstack/react-query"; import { useRoute, Link } from "wouter"; -import { App, Review } from "@/types"; -import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { Textarea } from "@/components/ui/textarea"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Separator } from "@/components/ui/separator"; -import { Progress } from "@/components/ui/progress"; -import { Star, ExternalLink, Share, ArrowLeft, Store } from "lucide-react"; -import { apiRequest, queryClient } from "@/lib/queryClient"; -import { useToast } from "@/hooks/use-toast"; +import { ExternalLink, ArrowLeft, Store } from "lucide-react"; +import appsData from "@/data/apps.json"; + +// Mock detailed descriptions for each app +const appDetails: Record = { + "eid-wallet": { + fullDescription: "eID for W3DS is a comprehensive digital identity solution that puts you in control of your personal information. Built on the W3DS framework, it provides secure authentication, verifiable credentials, and seamless integration across the MetaState ecosystem.\n\nWith advanced cryptographic protocols and user-centric design, eID for W3DS ensures your identity remains sovereign and under your control at all times.", + screenshots: [] + }, + "blabsy": { + fullDescription: "Blabsy is a decentralized micro-blogging platform where you own your content and control your data. Share your thoughts, engage with communities, and build your network while maintaining full sovereignty over your digital presence.\n\nExperience social media reimagined with privacy-first principles and community-driven governance.", + screenshots: [] + }, + "pictique": { + fullDescription: "Pictique revolutionizes photo sharing with privacy-first principles. Share your moments with complete control over who sees what, when, and for how long. All while maintaining ownership of your precious memories.\n\nBuilt on the W3DS framework, Pictique ensures your photos are stored securely and shared on your terms.", + screenshots: [] + }, + "evoting": { + fullDescription: "eVoting brings democracy to the digital age with end-to-end verifiable elections. Using advanced cryptographic techniques, every vote is counted accurately while maintaining voter privacy. Perfect for organizations, communities, and governance bodies.\n\nTransparent, secure, and verifiable - democracy as it should be.", + screenshots: [] + }, + "group-charter": { + fullDescription: "Charter Manager empowers communities to establish clear governance structures. Create charters, define rules, manage memberships, and ensure transparent decision-making processes. Build self-sovereign communities with accountable governance.\n\nFrom small groups to large organizations, Charter Manager provides the tools you need for effective community governance.", + screenshots: [] + }, + "dreamsync": { + fullDescription: "DreamSync helps you discover meaningful connections based on shared interests, values, and aspirations. Navigate the W3DS ecosystem to find individuals who resonate with your vision and collaborate on projects that matter.\n\nConnect, collaborate, and create together in the decentralized future.", + screenshots: [] + } +}; export default function AppDetailPage() { const [, params] = useRoute("/app/:id"); const appId = params?.id; - const [showReviewForm, setShowReviewForm] = useState(false); - const [reviewForm, setReviewForm] = useState({ - username: "", - rating: 5, - comment: "", - }); - const { toast } = useToast(); - - const { data: app, isLoading: isLoadingApp } = useQuery({ - queryKey: ["/api/apps", appId], - enabled: !!appId, - }); - - const { data: reviews = [], isLoading: isLoadingReviews } = useQuery({ - queryKey: ["/api/apps", appId, "reviews"], - enabled: !!appId, - }); - - const reviewMutation = useMutation({ - mutationFn: async (reviewData: typeof reviewForm) => { - const res = await apiRequest("POST", `/api/apps/${appId}/reviews`, reviewData); - return await res.json(); - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["/api/apps", appId, "reviews"] }); - queryClient.invalidateQueries({ queryKey: ["/api/apps", appId] }); - setShowReviewForm(false); - setReviewForm({ username: "", rating: 5, comment: "" }); - toast({ - title: "Review submitted!", - description: "Thank you for your feedback.", - }); - }, - onError: (error) => { - toast({ - title: "Failed to submit review", - description: error.message, - variant: "destructive", - }); - }, - }); - - const renderStars = (rating: number, interactive = false, onRate?: (rating: number) => void) => { - return ( -
- {[1, 2, 3, 4, 5].map((star) => ( - interactive && onRate?.(star)} - /> - ))} -
- ); - }; - const handleReviewSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (!reviewForm.username.trim()) { - toast({ - title: "Username required", - description: "Please enter your username.", - variant: "destructive", - }); - return; - } - reviewMutation.mutate(reviewForm); - }; + const app = appsData.find(a => a.id === appId); + const details = appId ? appDetails[appId] : null; - const getRatingDistribution = () => { - const distribution = { 5: 0, 4: 0, 3: 0, 2: 0, 1: 0 }; - reviews.forEach(review => { - distribution[review.rating as keyof typeof distribution]++; - }); - const total = reviews.length || 1; - return Object.entries(distribution).reverse().map(([rating, count]) => ({ - rating: parseInt(rating), - count, - percentage: (count / total) * 100, - })); - }; - - if (isLoadingApp) { - return ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ); - } if (!app) { return ( @@ -171,15 +85,6 @@ export default function AppDetailPage() {

{app.name}

{app.description}

-
- {renderStars(parseFloat(app.averageRating || "0"))} - - {parseFloat(app.averageRating || "0").toFixed(1)} - - - ({app.totalReviews} reviews) - -
{app.category}
@@ -188,42 +93,38 @@ export default function AppDetailPage() {
- - - - + {(app as any).appStoreUrl && (app as any).playStoreUrl ? ( + <> + + + + + + + + ) : ( + + + + )}
- {/* Screenshots */} - {app.screenshots && app.screenshots.length > 0 && ( -
-

Screenshots

-
- {app.screenshots.map((screenshot, index) => ( - {`${app.name} - ))} -
-
- )} - {/* Description */} - {app.fullDescription && ( + {details?.fullDescription && (

About this app

- {app.fullDescription.split('\n').map((paragraph, index) => ( + {details.fullDescription.split('\n').map((paragraph, index) => (

{paragraph}

@@ -232,7 +133,17 @@ export default function AppDetailPage() {
)} - {/* Reviews Section */} + {/* Reviews Section - Coming Soon */} +
+
+

Reviews & Ratings

+

Reviews feature coming soon

+
+
+ + {/* + Reviews section commented out for future implementation +

Reviews & Ratings

@@ -243,124 +154,9 @@ export default function AppDetailPage() { {showReviewForm ? "Cancel" : "Write Review"}
- - {/* Rating Summary */} -
-
-
-
- {parseFloat(app.averageRating || "0").toFixed(1)} -
-
- {renderStars(parseFloat(app.averageRating || "0"))} -
-
{app.totalReviews} reviews
-
-
-
- {getRatingDistribution().map(({ rating, count, percentage }) => ( -
- {rating} - - {Math.round(percentage)}% -
- ))} -
-
-
-
- - {/* Review Form */} - {showReviewForm && ( -
-
-
- - setReviewForm(prev => ({ ...prev, username: e.target.value }))} - placeholder="Enter your username" - className="h-12 rounded-full border-2 border-gray-200 focus:border-black font-medium" - required - /> -
-
- -
- {renderStars(reviewForm.rating, true, (rating) => - setReviewForm(prev => ({ ...prev, rating })) - )} - ({reviewForm.rating} stars) -
-
-
- -