From 3492b0f9369a4e07be924d6acb49a599d1f18c2f Mon Sep 17 00:00:00 2001 From: byteful Date: Sun, 4 Jan 2026 02:35:43 -0600 Subject: [PATCH 01/15] draft 1 --- .env.development | 11 + .env.production | 8 + client/modl_privacy_policy.md | 88 ++++++++ client/modl_terms_of_service.md | 118 +++++++++++ client/src/components/home/FAQSection.tsx | 6 +- .../registration/RegistrationForm.tsx | 2 +- client/src/lib/queryClient.ts | 16 +- package.json | 6 +- server/index.ts | 81 -------- server/routes.ts | 194 ------------------ server/storage.ts | 111 ---------- server/vite.ts | 85 -------- vite.config.ts | 10 + 13 files changed, 256 insertions(+), 480 deletions(-) create mode 100644 .env.development create mode 100644 .env.production create mode 100644 client/modl_privacy_policy.md create mode 100644 client/modl_terms_of_service.md delete mode 100644 server/index.ts delete mode 100644 server/routes.ts delete mode 100644 server/storage.ts delete mode 100644 server/vite.ts diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..b742e72 --- /dev/null +++ b/.env.development @@ -0,0 +1,11 @@ +# Development environment configuration +# This file is used when running: npm run dev + +# API URL - empty to use vite proxy to localhost:8080 +VITE_API_URL= + +# App domain for local testing +APP_DOMAIN=localhost + +# Turnstile test keys (Cloudflare provides test keys for development) +VITE_TURNSTILE_SITE_KEY=1x00000000000000000000AA diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..75eb747 --- /dev/null +++ b/.env.production @@ -0,0 +1,8 @@ +# Production environment configuration +# This file is used when running: npm run build + +# API URL - points to production API +VITE_API_URL=https://api.modl.gg + +# App domain +APP_DOMAIN=modl.gg diff --git a/client/modl_privacy_policy.md b/client/modl_privacy_policy.md new file mode 100644 index 0000000..74ed41c --- /dev/null +++ b/client/modl_privacy_policy.md @@ -0,0 +1,88 @@ +# Privacy Policy + +**Last Updated: January 2025** + +## 1. Introduction + +Welcome to modl.gg ("we," "our," or "us"). We are committed to protecting your privacy and ensuring you have a positive experience on our platform. This Privacy Policy explains how we collect, use, disclose, and safeguard your information when you use our moderation and support platform. + +## 2. Information We Collect + +### 2.1 Information You Provide + +- **Account Information**: When you register, we collect your email address and server information. +- **Server Data**: Information about your Minecraft server including server name, domain, and configuration. +- **Staff Information**: Data about staff members you add to your server panel. +- **Support Content**: Tickets, appeals, and other support-related content you create. + +### 2.2 Automatically Collected Information + +- **Usage Data**: Information about how you interact with our platform. +- **Log Data**: Server logs, IP addresses, browser type, and access times. +- **Device Information**: Information about the device you use to access our services. + +### 2.3 Player Data + +- **Minecraft UUIDs**: Player identifiers from your Minecraft server. +- **Moderation Records**: Punishment history, appeals, and related data. +- **Chat Logs**: If AI moderation is enabled, relevant chat content for analysis. + +## 3. How We Use Your Information + +We use the information we collect to: + +- Provide and maintain our moderation and support services +- Process and respond to support tickets and appeals +- Improve our platform and develop new features +- Communicate with you about updates and changes +- Ensure the security and integrity of our platform +- Comply with legal obligations + +## 4. Data Sharing and Disclosure + +We do not sell your personal information. We may share data with: + +- **Service Providers**: Third parties that help us operate our platform +- **Legal Requirements**: When required by law or to protect our rights +- **Business Transfers**: In connection with a merger, acquisition, or sale + +## 5. Data Security + +We implement appropriate technical and organizational measures to protect your data, including: + +- Encryption in transit and at rest +- Regular security assessments +- Access controls and authentication +- Secure data storage practices + +## 6. Data Retention + +We retain your data for as long as your account is active or as needed to provide services. You may request deletion of your data at any time. + +## 7. Your Rights + +You have the right to: + +- Access your personal data +- Correct inaccurate data +- Delete your data +- Export your data +- Withdraw consent + +## 8. Third-Party Services + +Our platform may integrate with third-party services. Please review their privacy policies as we are not responsible for their practices. + +## 9. Children's Privacy + +Our platform is not intended for users under 13 years of age. We do not knowingly collect data from children. + +## 10. Changes to This Policy + +We may update this Privacy Policy periodically. We will notify you of significant changes via email or through our platform. + +## 11. Contact Us + +If you have questions about this Privacy Policy, please contact us at: + +**Email**: support@modl.gg diff --git a/client/modl_terms_of_service.md b/client/modl_terms_of_service.md new file mode 100644 index 0000000..09f55fb --- /dev/null +++ b/client/modl_terms_of_service.md @@ -0,0 +1,118 @@ +# Terms of Service + +**Last Updated: January 2025** + +## 1. Agreement to Terms + +By accessing or using modl.gg ("Service"), you agree to be bound by these Terms of Service ("Terms"). If you do not agree to these Terms, please do not use the Service. + +## 2. Description of Service + +modl.gg provides a moderation and support platform for Minecraft server communities, including: + +- Player moderation tools and punishment management +- Support ticket and appeals systems +- Staff management and permissions +- Analytics and reporting +- AI-powered moderation assistance (Premium) + +## 3. Account Registration + +### 3.1 Eligibility + +You must be at least 13 years old to use our Service. By registering, you represent that you meet this requirement. + +### 3.2 Account Security + +You are responsible for maintaining the security of your account credentials. You must notify us immediately of any unauthorized access. + +### 3.3 Accurate Information + +You agree to provide accurate, current, and complete information during registration and to update such information as necessary. + +## 4. Acceptable Use + +You agree NOT to: + +- Use the Service for any illegal purpose +- Abuse, harass, or discriminate against users +- Attempt to gain unauthorized access to our systems +- Interfere with or disrupt the Service +- Upload malicious code or content +- Violate the rights of others +- Resell or redistribute the Service without permission + +## 5. Server Owner Responsibilities + +As a server owner using our Service, you are responsible for: + +- Compliance with Minecraft's EULA and terms +- Your staff's actions within the platform +- Data you collect and store about your players +- Ensuring your moderation practices are fair and lawful + +## 6. Subscription and Payments + +### 6.1 Free Plan + +The Free Plan includes limited features and is subject to usage limits as described on our pricing page. + +### 6.2 Premium Plan + +Premium subscriptions are billed monthly. You may cancel at any time, with access continuing until the end of the billing period. + +### 6.3 Refunds + +We offer refunds within 14 days of initial purchase if you are not satisfied with the Service. + +## 7. Intellectual Property + +### 7.1 Our Property + +The Service, including its design, features, and content, is owned by modl.gg and protected by intellectual property laws. + +### 7.2 Your Content + +You retain ownership of content you upload. By using the Service, you grant us a license to use, store, and process your content as necessary to provide the Service. + +## 8. Privacy + +Your use of the Service is also governed by our Privacy Policy, which is incorporated into these Terms. + +## 9. Disclaimer of Warranties + +THE SERVICE IS PROVIDED "AS IS" WITHOUT WARRANTIES OF ANY KIND. WE DO NOT GUARANTEE UNINTERRUPTED OR ERROR-FREE SERVICE. + +## 10. Limitation of Liability + +TO THE MAXIMUM EXTENT PERMITTED BY LAW, MODL.GG SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES. + +## 11. Indemnification + +You agree to indemnify and hold harmless modl.gg from any claims arising from your use of the Service or violation of these Terms. + +## 12. Termination + +We may suspend or terminate your access to the Service at any time for violation of these Terms or for any other reason at our discretion. + +## 13. Changes to Terms + +We may modify these Terms at any time. Continued use of the Service after changes constitutes acceptance of the modified Terms. + +## 14. Governing Law + +These Terms are governed by the laws of the jurisdiction in which modl.gg operates, without regard to conflict of law principles. + +## 15. Dispute Resolution + +Any disputes shall be resolved through binding arbitration, except where prohibited by law. + +## 16. Severability + +If any provision of these Terms is found unenforceable, the remaining provisions shall continue in effect. + +## 17. Contact + +For questions about these Terms, please contact us at: + +**Email**: support@modl.gg diff --git a/client/src/components/home/FAQSection.tsx b/client/src/components/home/FAQSection.tsx index f0c60ad..7863419 100644 --- a/client/src/components/home/FAQSection.tsx +++ b/client/src/components/home/FAQSection.tsx @@ -1,11 +1,11 @@ -import { useState } from "react"; -import { motion, AnimatePresence } from "framer-motion"; +import { motion } from "framer-motion"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@modl-gg/shared-web/components/ui/accordion"; +import { MODL } from "@modl-gg/shared-web"; const FAQData = [ { @@ -90,7 +90,7 @@ export default function FAQSection() { -

Contact us at support@modl.gg

+

Contact us at {MODL.Email.SUPPORT}

); } diff --git a/client/src/components/registration/RegistrationForm.tsx b/client/src/components/registration/RegistrationForm.tsx index 90084d6..8d8e4bd 100644 --- a/client/src/components/registration/RegistrationForm.tsx +++ b/client/src/components/registration/RegistrationForm.tsx @@ -127,7 +127,7 @@ export default function RegistrationForm() { ...values, plan: "free" as const }; - const res = await apiRequest("POST", "/api/register", submitData); + const res = await apiRequest("POST", "/v1/public/registration", submitData); if (res.ok) { setShowSuccess(true); setRegisteredDomain(values.customDomain); // Save the custom domain diff --git a/client/src/lib/queryClient.ts b/client/src/lib/queryClient.ts index a8b3fc1..bb81b7b 100644 --- a/client/src/lib/queryClient.ts +++ b/client/src/lib/queryClient.ts @@ -1,4 +1,13 @@ import { QueryClient, QueryFunction } from "@tanstack/react-query"; +import { MODL } from "@modl-gg/shared-web"; + +const API_BASE_URL = import.meta.env.DEV + ? '' + : (import.meta.env.VITE_API_URL || MODL.Domain.HTTPS_API); + +export function getApiUrl(path: string): string { + return `${API_BASE_URL}${path}`; +} async function throwIfResNotOk(res: Response) { if (!res.ok) { @@ -12,7 +21,8 @@ export async function apiRequest( url: string, data?: unknown | undefined, ): Promise { - const res = await fetch(url, { + const fullUrl = url.startsWith('http') ? url : getApiUrl(url); + const res = await fetch(fullUrl, { method, headers: data ? { "Content-Type": "application/json" } : {}, body: data ? JSON.stringify(data) : undefined, @@ -29,7 +39,9 @@ export const getQueryFn: (options: { }) => QueryFunction = ({ on401: unauthorizedBehavior }) => async ({ queryKey }) => { - const res = await fetch(queryKey[0] as string, { + const url = queryKey[0] as string; + const fullUrl = url.startsWith('http') ? url : getApiUrl(url); + const res = await fetch(fullUrl, { credentials: "include", }); diff --git a/package.json b/package.json index 594b07b..d630974 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,9 @@ "type": "module", "license": "AGPL-3.0-only", "scripts": { - "dev": "cross-env 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", + "dev": "vite", + "build": "vite build", + "preview": "vite preview", "check": "tsc" }, "dependencies": { diff --git a/server/index.ts b/server/index.ts deleted file mode 100644 index 3790271..0000000 --- a/server/index.ts +++ /dev/null @@ -1,81 +0,0 @@ -import express, { type Request, Response, NextFunction } from "express"; -import { registerRoutes } from "./routes"; -import { setupVite, serveStatic, log } from "./vite"; -import path from "path"; -import 'dotenv/config'; - -const app = express(); -app.set('trust proxy', true); // Trust X-Forwarded-For headers from proxies -app.use(express.json()); -app.use(express.urlencoded({ extended: false })); - -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); - } - }); - - next(); -}); - -(async () => { - const server = await registerRoutes(app); - - // Serve markdown files from the attached_assets directory - app.get('/modl_terms_of_service.md', (req: Request, res: Response) => { - res.sendFile(path.join(process.cwd(), 'attached_assets', 'modl_terms_of_service.md')); - }); - - app.get('/modl_privacy_policy.md', (req: Request, res: Response) => { - res.sendFile(path.join(process.cwd(), 'attached_assets', 'modl_privacy_policy.md')); - }); - - 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; - }); - - // 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 - if (app.get("env") === "development") { - await setupVite(app, server); - } else { - serveStatic(app); - } - - // ALWAYS serve the app on port 5000 - // this serves both the API and the client. - // It is the only port that is not firewalled. - const port = app.get("env") === "development" ? 3000 : 6000; - server.listen({ - port, - host: "0.0.0.0" - }, () => { - log(`serving on port ${port}`); - }); -})(); diff --git a/server/routes.ts b/server/routes.ts deleted file mode 100644 index 2095c56..0000000 --- a/server/routes.ts +++ /dev/null @@ -1,194 +0,0 @@ -import type { Express } from "express"; -import { createServer, type Server } from "http"; -import { storage } from "./storage"; -import { z } from "zod"; -import nodemailer from "nodemailer"; // Added -import crypto from "crypto"; // Added -import 'dotenv/config'; - -// Turnstile validation function -async function validateTurnstileToken(token: string, remoteip?: string): Promise { - try { - const secretKey = process.env.TURNSTILE_SECRET_KEY; - if (!secretKey) { - console.error('TURNSTILE_SECRET_KEY is not set in environment variables'); - return false; - } - - const formData = new URLSearchParams(); - formData.append('secret', secretKey); - formData.append('response', token); - if (remoteip) { - formData.append('remoteip', remoteip); - } - - const response = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: formData, - }); - - const result = await response.json(); - return result.success === true; - } catch (error) { - console.error('Error validating Turnstile token:', error); - return false; - } -} - -// Registration schema that allows spaces in server names -const registrationSchema = z.object({ - email: z.string().email({ message: "Please enter a valid email address" }), - serverName: z.string() - .min(3, { message: "Server name is required (min 3 characters)" }) - .max(100, { message: "Server name must be less than 100 characters" }) - .trim(), - customDomain: z.string() - .min(3, { message: "Subdomain is required (min 3 characters)" }) - .max(50, { message: "Subdomain must be less than 50 characters" }) - .regex(/^[a-z0-9-]+$/, { message: "Subdomain can only contain lowercase letters, numbers, and hyphens" }) - .trim(), - plan: z.enum(["free", "premium"]).default("free"), - turnstileToken: z.string().min(1, { message: "Security verification is required" }), -}); - -// Rate limiting for registration - stores IP addresses and their last registration time -const registrationRateLimit = new Map(); -const RATE_LIMIT_WINDOW_MS = 10 * 60 * 1000; // 10 minutes in milliseconds - -export async function registerRoutes(app: Express): Promise { - // Registration endpoint - app.post("/api/register", async (req, res) => { - try { - // Get client IP address - const clientIP = req.ip || req.connection.remoteAddress || req.socket.remoteAddress || - (req.connection as any)?.socket?.remoteAddress || 'unknown'; - - console.log(`Registration attempt from IP: ${clientIP}`); - console.log(`Current rate limit map size: ${registrationRateLimit.size}`); - console.log(`Rate limit entries:`, Array.from(registrationRateLimit.entries())); - - // Check rate limit - const now = Date.now(); - const lastRegistration = registrationRateLimit.get(clientIP); - - console.log(`Last registration for IP ${clientIP}: ${lastRegistration}`); - console.log(`Time since last registration: ${lastRegistration ? now - lastRegistration : 'N/A'}ms`); - - if (lastRegistration && (now - lastRegistration) < RATE_LIMIT_WINDOW_MS) { - const remainingTime = Math.ceil((RATE_LIMIT_WINDOW_MS - (now - lastRegistration)) / 1000 / 60); - console.log(`Rate limit triggered for IP ${clientIP}, remaining time: ${remainingTime} minutes`); - return res.status(429).json({ - success: false, - message: `Rate limit exceeded. You can only register one server every 10 minutes. Please try again in ${remainingTime} minute${remainingTime !== 1 ? 's' : ''}.` - }); - } - - const validatedData = registrationSchema.parse(req.body); - - // Validate Turnstile token - const turnstileValid = await validateTurnstileToken(validatedData.turnstileToken, clientIP); - if (!turnstileValid) { - console.log(`Turnstile validation failed for IP ${clientIP}`); - return res.status(400).json({ - success: false, - message: "Security verification failed. Please try again." - }); - } - - const emailVerificationToken = crypto.randomBytes(32).toString("hex"); // Added - - // Create the server entry in MongoDB - const serverInfo = await storage.createServer({ - adminEmail: validatedData.email, - serverName: validatedData.serverName, - customDomain: validatedData.customDomain, - plan: validatedData.plan, - emailVerificationToken, // Added - emailVerified: false, // Added - }); - - // Update rate limit tracking - only after successful registration - registrationRateLimit.set(clientIP, now); - console.log(`Rate limit updated for IP ${clientIP} at ${now}`); - - // Clean up old entries (optional optimization) - const sizeBefore = registrationRateLimit.size; - registrationRateLimit.forEach((timestamp, ip) => { - if (now - timestamp > RATE_LIMIT_WINDOW_MS) { - registrationRateLimit.delete(ip); - } - }); - console.log(`Rate limit cleanup: ${sizeBefore} -> ${registrationRateLimit.size} entries`); - - // Send verification email - const smtpPort = Number(process.env.SMTP_PORT) || 25; - const emailAuth = { - user: process.env.SMTP_USERNAME, - pass: process.env.SMTP_PASSWORD - }; - const transporter = nodemailer.createTransport({ - host: process.env.SMTP_HOST || "localhost", // Assuming postfix is running on localhost - port: smtpPort, - secure: false, // true for 465, false for other ports - tls: { - rejectUnauthorized: false // Allow self-signed certificates - }, - auth: (emailAuth.user && emailAuth.pass) ? emailAuth : undefined - }); - - const appDomain = process.env.APP_DOMAIN || "modl.gg"; - const verificationLink = `https://${serverInfo.customDomain}.${appDomain}/verify-email?token=${emailVerificationToken}`; - - await transporter.verify(); - await transporter.sendMail({ - from: `"modl" `, // sender address - to: validatedData.email, // list of receivers - subject: "Verify Your Email Address for modl", // Subject line - text: `Please verify your email address by clicking the following link: ${verificationLink}`, // plain text body - html: `

Please verify your email address by clicking the following link: ${verificationLink}

`, // html body - }); - - return res.status(201).json({ - success: true, - message: "Registration successful. Please check your email to verify your account.", // Updated message - server: { - id: serverInfo.id, - name: serverInfo.serverName - } - }); - } catch (error) { - if (error instanceof z.ZodError) { - // Create a more user-friendly validation error message - const fieldErrors = error.errors.map(err => { - const field = err.path.join('.'); - return `${field}: ${err.message}`; - }).join(', '); - - return res.status(400).json({ - success: false, - message: `Validation failed: ${fieldErrors}`, - errors: error.errors - }); - } - // Handle duplicate entry errors from storage.createServer - if (error instanceof Error && error.message.startsWith("DUPLICATE_ENTRY:")) { - return res.status(409).json({ - success: false, - message: error.message.replace("DUPLICATE_ENTRY: ", ""), // More user-friendly message - }); - } - - console.error("Registration error:", error); - return res.status(500).json({ - success: false, - message: "An internal server error occurred during registration. Please try again later. If the issue persists, contact support." - }); - } - }); - - const httpServer = createServer(app); - return httpServer; -} diff --git a/server/storage.ts b/server/storage.ts deleted file mode 100644 index 8e7dd4a..0000000 --- a/server/storage.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { InsertServer, Server } from "@modl-gg/shared-web"; -import { MongoClient } from "mongodb"; -import 'dotenv/config'; - -// modify the interface with any CRUD methods -// you might need - -export interface IStorage { - // getUser(id: number): Promise; // Removed - // getUserByUsername(username: string): Promise; // Removed - // createUser(user: InsertUser): Promise; // Removed - createServer(serverData: InsertServer): Promise; -} - -export class MemStorage implements IStorage { - // private users: Map; // Removed - private servers: Map; // Changed to string for ObjectId - // currentId: number; // Removed - // currentServerId: number; // No longer needed as MongoDB handles ID - private mongoClient: MongoClient; - private readonly mongoDbName = "modl"; - - constructor() { - // this.users = new Map(); // Removed - this.servers = new Map(); - // this.currentId = 1; // Removed - // this.currentServerId = 1; - // Replace with your MongoDB connection string - const uri = process.env.MONGODB_URI || "mongodb://localhost:27017"; - this.mongoClient = new MongoClient(uri, { - maxPoolSize: 50, // Maintain up to 50 socket connections - serverSelectionTimeoutMS: 5000, // Keep trying to send operations for 5 seconds - socketTimeoutMS: 45000, // Close sockets after 45 seconds of inactivity - connectTimeoutMS: 10000 // Give up initial connection after 10 seconds - }); - this.connectMongo(); - } - - private async connectMongo() { - try { - await this.mongoClient.connect(); - console.log("Connected to MongoDB"); - } catch (error) { - console.error("Failed to connect to MongoDB", error); - // Throw an error to prevent application startup if database is critical - throw new Error(`MongoDB Connection Error: ${error instanceof Error ? error.message : 'Unknown error'}`); - } - } - - // async getUser(id: number): Promise { // Removed - // return this.users.get(id); - // } - - // async getUserByUsername(username: string): Promise { // Removed - // return Array.from(this.users.values()).find( - // (user) => user.username === username, - // ); - // } - - // async createUser(insertUser: InsertUser): Promise { // Removed - // const id = this.currentId++; - // const user: User = { ...insertUser, id }; - // this.users.set(id, user); - // return user; - // } - - async createServer(serverData: InsertServer): Promise { - const db = this.mongoClient.db(this.mongoDbName); - const serversCollection = db.collection("servers"); - - // Check for duplicate entries - const existingServer = await serversCollection.findOne({ - $or: [ - { adminEmail: serverData.adminEmail }, - { serverName: serverData.serverName }, - { customDomain: serverData.customDomain }, - ], - }); - - if (existingServer) { - if (existingServer.adminEmail === serverData.adminEmail) { - throw new Error("DUPLICATE_ENTRY: An account with this email already exists."); - } - if (existingServer.serverName === serverData.serverName) { - throw new Error("DUPLICATE_ENTRY: This server name is already taken."); - } - if (existingServer.customDomain === serverData.customDomain) { - throw new Error("DUPLICATE_ENTRY: This subdomain is already in use."); - } - // Fallback for an unexpected match - throw new Error("DUPLICATE_ENTRY: A similar server configuration already exists."); - } - - const newServerDocument = { - ...serverData, - // _id is now generated by MongoDB - }; - - const result = await serversCollection.insertOne(newServerDocument); - const insertedId = result.insertedId; // Get the ObjectId generated by MongoDB - - const server: Server = { - id: insertedId.toHexString(), - ...serverData - }; - // this.servers.set(insertedId.toHexString(), server); // Also keep in-memory if needed, or rely solely on MongoDB - return server; - } -} - -export const storage = new MemStorage(); diff --git a/server/vite.ts b/server/vite.ts deleted file mode 100644 index 95395a0..0000000 --- a/server/vite.ts +++ /dev/null @@ -1,85 +0,0 @@ -import express, { type Express } from "express"; -import fs from "fs"; -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(); - -export function log(message: string, source = "express") { - const formattedTime = new Date().toLocaleTimeString("en-US", { - hour: "numeric", - minute: "2-digit", - second: "2-digit", - hour12: true, - }); - - console.log(`${formattedTime} [${source}] ${message}`); -} - -export async function setupVite(app: Express, server: Server) { - const serverOptions = { - middlewareMode: true, - hmr: { server }, - allowedHosts: true, - }; - - const vite = await createViteServer({ - ...viteConfig, - configFile: false, - customLogger: { - ...viteLogger, - error: (msg, options) => { - viteLogger.error(msg, options); - process.exit(1); - }, - }, - server: serverOptions, - appType: "custom", - }); - - app.use(vite.middlewares); - app.use("*", async (req, res, next) => { - const url = req.originalUrl; - - try { - const clientTemplate = path.resolve( - import.meta.dirname, - "..", - "client", - "index.html", - ); - - // always reload the index.html file from disk incase it changes - let template = await fs.promises.readFile(clientTemplate, "utf-8"); - template = template.replace( - `src="/src/main.tsx"`, - `src="/src/main.tsx?v=${nanoid()}"`, - ); - const page = await vite.transformIndexHtml(url, template); - res.status(200).set({ "Content-Type": "text/html" }).end(page); - } catch (e) { - vite.ssrFixStacktrace(e as Error); - next(e); - } - }); -} - -export function serveStatic(app: Express) { - const distPath = path.resolve(import.meta.dirname, "public"); - - if (!fs.existsSync(distPath)) { - throw new Error( - `Could not find the build directory: ${distPath}, make sure to build the client first`, - ); - } - - app.use(express.static(distPath)); - - // fall through to index.html if the file doesn't exist - app.use("*", (_req, res) => { - res.sendFile(path.resolve(distPath, "index.html")); - }); -} diff --git a/vite.config.ts b/vite.config.ts index 9e0bdaa..2f5d4f2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -33,4 +33,14 @@ export default defineConfig({ outDir: path.resolve(import.meta.dirname, "dist/public"), emptyOutDir: true, }, + server: { + port: 5174, + proxy: { + '/v1': { + target: 'http://localhost:8080', + changeOrigin: true, + secure: false, + }, + }, + }, }); From 9451b2b1b9f1b5dd82ff4ade6013855933f2c083 Mon Sep 17 00:00:00 2001 From: byteful Date: Tue, 20 Jan 2026 18:10:10 -0500 Subject: [PATCH 02/15] Design overhaul --- .env.development | 2 +- .env.production | 2 +- client/src/components/home/FAQSection.tsx | 53 +- .../src/components/home/FeaturesSection.tsx | 72 +- client/src/components/home/Footer.tsx | 147 +- client/src/components/home/HeroSection.tsx | 91 +- client/src/components/home/Navbar.tsx | 84 +- client/src/components/home/PricingSection.tsx | 153 +- client/src/index.css | 22 +- client/src/lib/queryClient.ts | 42 +- client/src/pages/Home.tsx | 25 +- package-lock.json | 1717 +---------------- package.json | 23 +- tailwind.config.ts | 4 +- vite.config.ts | 80 +- 15 files changed, 446 insertions(+), 2071 deletions(-) diff --git a/.env.development b/.env.development index b742e72..ac4f910 100644 --- a/.env.development +++ b/.env.development @@ -2,7 +2,7 @@ # This file is used when running: npm run dev # API URL - empty to use vite proxy to localhost:8080 -VITE_API_URL= +VITE_API_BASE_URL= # App domain for local testing APP_DOMAIN=localhost diff --git a/.env.production b/.env.production index 75eb747..2f32532 100644 --- a/.env.production +++ b/.env.production @@ -2,7 +2,7 @@ # This file is used when running: npm run build # API URL - points to production API -VITE_API_URL=https://api.modl.gg +VITE_API_BASE_URL=https://api.modl.gg # App domain APP_DOMAIN=modl.gg diff --git a/client/src/components/home/FAQSection.tsx b/client/src/components/home/FAQSection.tsx index 7863419..6e4afd2 100644 --- a/client/src/components/home/FAQSection.tsx +++ b/client/src/components/home/FAQSection.tsx @@ -5,7 +5,6 @@ import { AccordionItem, AccordionTrigger, } from "@modl-gg/shared-web/components/ui/accordion"; -import { MODL } from "@modl-gg/shared-web"; const FAQData = [ { @@ -40,31 +39,25 @@ const FAQData = [ export default function FAQSection() { return ( -
-
- -
- +
+ -

Frequently Asked Questions

-

- Got questions? We've got answers. -

+

Frequently Asked Questions

- - - + {FAQData.map((faq, index) => ( -
- - {faq.question} +
+ + {faq.question} -

+

{faq.answer}

@@ -88,9 +81,27 @@ export default function FAQSection() { ))} -
-

Contact us at {MODL.Email.SUPPORT}

+ +

+ Still have questions?{" "} + + Join our Discord community + +

+
+
); } diff --git a/client/src/components/home/FeaturesSection.tsx b/client/src/components/home/FeaturesSection.tsx index 7b5352f..ea6f0e8 100644 --- a/client/src/components/home/FeaturesSection.tsx +++ b/client/src/components/home/FeaturesSection.tsx @@ -1,9 +1,8 @@ import { useState } from "react"; import { motion, AnimatePresence } from "framer-motion"; -import { Check, ChevronDown, X, Plus } from "lucide-react"; +import { Check, X, Plus } from "lucide-react"; import { Button } from "@modl-gg/shared-web/components/ui/button"; -// Define the features array with detailed content const features = [ { icon: "shield-check", @@ -16,18 +15,18 @@ const features = [ { icon: "link", title: "Account Linking", - description: "Never issue a ban evasion punishment ever again or mangle with ip-bans .", + description: "Never issue a ban evasion punishment ever again or mangle with ip-bans.", expandedContent: "Automatically link accounts and issue alt-blocking punishments. Handle each linked ban independently, allowing for effortless appeals of siblings and other exceptions.", bgColor: "bg-accent/20", - textColor: "text-neutral-400" + textColor: "text-accent" }, { icon: "headphones", title: "Support Tickets", description: "Integrated ticketing system for reports, appeals, bugs, applications, and support", expandedContent: "Customize forms, quick-responses, notify players in-game and via email of ticket updates, and monitor how response times. For example, appeals automatically gather punishment information and you can pardon/reduce bans with 1 click.", - bgColor: "bg-green-500/20", - textColor: "text-green-500" + bgColor: "bg-emerald-500/20", + textColor: "text-emerald-400" }, { icon: "bot", @@ -43,19 +42,18 @@ const features = [ description: "Stay in control of your server anywhere you go with the web.", expandedContent: "A web interface that allows you to manage your server more efficiently than ever before alongside comprehensive in-game tools.", bgColor: "bg-accent/20", - textColor: "text-neutral-400" + textColor: "text-accent" }, { icon: "layout-dashboard", title: "Analytics Dashboard", description: "Comprehensive reports and insights on trends and moderator activity.", expandedContent: "Our analytics dashboard gives you deep insights into moderation trends, support trends, and staff activity. Take actions such as rolling back staff punishments, adjusting punishment lengths, and more to combat bad actors.", - bgColor: "bg-green-500/20", - textColor: "text-green-500" + bgColor: "bg-emerald-500/20", + textColor: "text-emerald-400" } ]; -// Icon mapping const getIcon = (iconName: string) => { switch (iconName) { case "shield-check": @@ -73,7 +71,7 @@ const getIcon = (iconName: string) => { case "headphones": return ( - + ); case "bot": @@ -111,13 +109,9 @@ export default function FeaturesSection() { }; return ( -
-
-
-
- -
- +
+

Powerful Moderation Tools

-

+

Everything you need to keep your Minecraft community safe, engaged, and supported

- -
+ +
{features.map((feature, index) => ( -
-
+
{getIcon(feature.icon)} @@ -149,15 +143,15 @@ export default function FeaturesSection() {
-

{feature.title}

-

{feature.description}

- +

{feature.title}

+

{feature.description}

+ {expandedFeature === index && ( -
-

+

+

{feature.expandedContent}

@@ -178,16 +172,15 @@ export default function FeaturesSection() { ))}
- - {/* Integration Features */} - -
+

Easy Integration with Your Server

{[ @@ -196,21 +189,20 @@ export default function FeaturesSection() { { text: "Fully configurable messages and settings", delay: 0.3 }, { text: "Low latency web-to-game sync", delay: 0.4 } ].map((item, index) => ( - - - {item.text} + + {item.text} ))} -
-

* Importing from other moderation plugins is available upon request

+

* Importing from other moderation plugins is available upon request

diff --git a/client/src/components/home/Footer.tsx b/client/src/components/home/Footer.tsx index aad0d24..2c00bc4 100644 --- a/client/src/components/home/Footer.tsx +++ b/client/src/components/home/Footer.tsx @@ -1,58 +1,109 @@ -import { motion } from "framer-motion"; +import { Github } from "lucide-react"; +import { FaDiscord } from "react-icons/fa"; export default function Footer() { + const scrollToSection = (id: string) => { + const element = document.getElementById(id); + if (element) { + element.scrollIntoView({ behavior: "smooth" }); + } + }; + return ( -
+
+ ); } diff --git a/client/src/components/home/HeroSection.tsx b/client/src/components/home/HeroSection.tsx index c3b51db..703bccc 100644 --- a/client/src/components/home/HeroSection.tsx +++ b/client/src/components/home/HeroSection.tsx @@ -1,7 +1,6 @@ import { useLocation } from "wouter"; -import { Button } from "@modl-gg/shared-web/components/ui/button"; import { motion } from "framer-motion"; -import { Check, ArrowDown } from "lucide-react"; +import { ArrowRight, BookOpen } from "lucide-react"; export default function HeroSection() { const [, navigate] = useLocation(); @@ -18,76 +17,32 @@ export default function HeroSection() { }; return ( -
- {/* Background gradients */} -
-
- -
- +
+ -
-

- First-class -

- - Moderation & Support - -

- Suite For Minecraft -

-

- Streamline moderation and support through seamless integration. -

-
- - -
-
- - Free forever, no card or gimmicks -
-
-
- - - {/* Mission statement section */} - -
-
- OUR MISSION -
-

For server owners, by server owners

-

- Coming from a Minecraft server owner background, we built modl.gg to streamline - the numerous inefficiencies that hold back effective moderation and support - at scale for gaming communities. Utilizing fully dynamic punishments, AI - integration, and a beautiful web panel, we aim to make moderation and support painless - for your team and your players. -

+

+ First-class Moderation & Support Suite for Minecraft +

+

+ Streamline moderation and support through seamless integration. Everything you need to keep your community safe, engaged, and supported. +

+
+ +
diff --git a/client/src/components/home/Navbar.tsx b/client/src/components/home/Navbar.tsx index 898fbf8..1fe484c 100644 --- a/client/src/components/home/Navbar.tsx +++ b/client/src/components/home/Navbar.tsx @@ -1,6 +1,5 @@ import { useEffect, useState } from "react"; import { useLocation } from "wouter"; -import { Button } from "@modl-gg/shared-web/components/ui/button"; export default function Navbar() { const [isScrolled, setIsScrolled] = useState(false); @@ -10,10 +9,10 @@ export default function Navbar() { const handleScroll = () => { setIsScrolled(window.scrollY > 10); }; - + window.addEventListener("scroll", handleScroll); - handleScroll(); // Check initially - + handleScroll(); + return () => window.removeEventListener("scroll", handleScroll); }, []); @@ -29,31 +28,58 @@ export default function Navbar() { }; return ( -