Set up the basic structure and functionality for the DJ management system
Initializes project structure, adds core components, and configures essential dependencies. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 3a22ac80-cd1d-4441-9e36-f24fc2f4c3de Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3478f7c3-db8c-4fca-9165-3adbdf1b5829/e8da43e7-d99c-4328-9fdc-485bdeecffc1.jpg
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
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 });
|
||||
@@ -0,0 +1,70 @@
|
||||
import express, { type Request, Response, NextFunction } from "express";
|
||||
import { registerRoutes } from "./routes";
|
||||
import { setupVite, serveStatic, log } from "./vite";
|
||||
|
||||
const app = express();
|
||||
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<string, any> | 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);
|
||||
|
||||
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 = 5000;
|
||||
server.listen({
|
||||
port,
|
||||
host: "0.0.0.0",
|
||||
reusePort: true,
|
||||
}, () => {
|
||||
log(`serving on port ${port}`);
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,157 @@
|
||||
import * as client from "openid-client";
|
||||
import { Strategy, type VerifyFunction } from "openid-client/passport";
|
||||
|
||||
import passport from "passport";
|
||||
import session from "express-session";
|
||||
import type { Express, RequestHandler } from "express";
|
||||
import memoize from "memoizee";
|
||||
import connectPg from "connect-pg-simple";
|
||||
import { storage } from "./storage";
|
||||
|
||||
if (!process.env.REPLIT_DOMAINS) {
|
||||
throw new Error("Environment variable REPLIT_DOMAINS not provided");
|
||||
}
|
||||
|
||||
const getOidcConfig = memoize(
|
||||
async () => {
|
||||
return await client.discovery(
|
||||
new URL(process.env.ISSUER_URL ?? "https://replit.com/oidc"),
|
||||
process.env.REPL_ID!
|
||||
);
|
||||
},
|
||||
{ maxAge: 3600 * 1000 }
|
||||
);
|
||||
|
||||
export function getSession() {
|
||||
const sessionTtl = 7 * 24 * 60 * 60 * 1000; // 1 week
|
||||
const pgStore = connectPg(session);
|
||||
const sessionStore = new pgStore({
|
||||
conString: process.env.DATABASE_URL,
|
||||
createTableIfMissing: false,
|
||||
ttl: sessionTtl,
|
||||
tableName: "sessions",
|
||||
});
|
||||
return session({
|
||||
secret: process.env.SESSION_SECRET!,
|
||||
store: sessionStore,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
maxAge: sessionTtl,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function updateUserSession(
|
||||
user: any,
|
||||
tokens: client.TokenEndpointResponse & client.TokenEndpointResponseHelpers
|
||||
) {
|
||||
user.claims = tokens.claims();
|
||||
user.access_token = tokens.access_token;
|
||||
user.refresh_token = tokens.refresh_token;
|
||||
user.expires_at = user.claims?.exp;
|
||||
}
|
||||
|
||||
async function upsertUser(
|
||||
claims: any,
|
||||
) {
|
||||
await storage.upsertUser({
|
||||
id: claims["sub"],
|
||||
email: claims["email"],
|
||||
firstName: claims["first_name"],
|
||||
lastName: claims["last_name"],
|
||||
profileImageUrl: claims["profile_image_url"],
|
||||
});
|
||||
}
|
||||
|
||||
export async function setupAuth(app: Express) {
|
||||
app.set("trust proxy", 1);
|
||||
app.use(getSession());
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
const config = await getOidcConfig();
|
||||
|
||||
const verify: VerifyFunction = async (
|
||||
tokens: client.TokenEndpointResponse & client.TokenEndpointResponseHelpers,
|
||||
verified: passport.AuthenticateCallback
|
||||
) => {
|
||||
const user = {};
|
||||
updateUserSession(user, tokens);
|
||||
await upsertUser(tokens.claims());
|
||||
verified(null, user);
|
||||
};
|
||||
|
||||
for (const domain of process.env
|
||||
.REPLIT_DOMAINS!.split(",")) {
|
||||
const strategy = new Strategy(
|
||||
{
|
||||
name: `replitauth:${domain}`,
|
||||
config,
|
||||
scope: "openid email profile offline_access",
|
||||
callbackURL: `https://${domain}/api/callback`,
|
||||
},
|
||||
verify,
|
||||
);
|
||||
passport.use(strategy);
|
||||
}
|
||||
|
||||
passport.serializeUser((user: Express.User, cb) => cb(null, user));
|
||||
passport.deserializeUser((user: Express.User, cb) => cb(null, user));
|
||||
|
||||
app.get("/api/login", (req, res, next) => {
|
||||
passport.authenticate(`replitauth:${req.hostname}`, {
|
||||
prompt: "login consent",
|
||||
scope: ["openid", "email", "profile", "offline_access"],
|
||||
})(req, res, next);
|
||||
});
|
||||
|
||||
app.get("/api/callback", (req, res, next) => {
|
||||
passport.authenticate(`replitauth:${req.hostname}`, {
|
||||
successReturnToOrRedirect: "/",
|
||||
failureRedirect: "/api/login",
|
||||
})(req, res, next);
|
||||
});
|
||||
|
||||
app.get("/api/logout", (req, res) => {
|
||||
req.logout(() => {
|
||||
res.redirect(
|
||||
client.buildEndSessionUrl(config, {
|
||||
client_id: process.env.REPL_ID!,
|
||||
post_logout_redirect_uri: `${req.protocol}://${req.hostname}`,
|
||||
}).href
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export const isAuthenticated: RequestHandler = async (req, res, next) => {
|
||||
const user = req.user as any;
|
||||
|
||||
if (!req.isAuthenticated() || !user.expires_at) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
if (now <= user.expires_at) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const refreshToken = user.refresh_token;
|
||||
if (!refreshToken) {
|
||||
res.status(401).json({ message: "Unauthorized" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const config = await getOidcConfig();
|
||||
const tokenResponse = await client.refreshTokenGrant(config, refreshToken);
|
||||
updateUserSession(user, tokenResponse);
|
||||
return next();
|
||||
} catch (error) {
|
||||
res.status(401).json({ message: "Unauthorized" });
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,566 @@
|
||||
import type { Express } from "express";
|
||||
import { createServer, type Server } from "http";
|
||||
import { storage } from "./storage";
|
||||
import { setupAuth, isAuthenticated } from "./replitAuth";
|
||||
import {
|
||||
insertEventSchema,
|
||||
insertEventTypeSchema,
|
||||
insertScheduleTemplateSchema,
|
||||
insertScheduleTemplateSlotSchema,
|
||||
insertSocialLinkSchema,
|
||||
insertAvailabilitySchema,
|
||||
insertRemovalRequestSchema,
|
||||
insertInvitationSchema,
|
||||
updateUserSchema,
|
||||
updateEventSchema,
|
||||
updateEventTypeSchema,
|
||||
updateScheduleTemplateSchema,
|
||||
updateRemovalRequestSchema,
|
||||
} from "@shared/schema";
|
||||
import { z } from "zod";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import crypto from "crypto";
|
||||
|
||||
export async function registerRoutes(app: Express): Promise<Server> {
|
||||
// Auth middleware
|
||||
await setupAuth(app);
|
||||
|
||||
// Helper function to check if user is admin
|
||||
const isAdmin = async (req: any, res: any, next: any) => {
|
||||
try {
|
||||
const userId = req.user.claims.sub;
|
||||
const user = await storage.getUser(userId);
|
||||
if (!user || user.role !== "admin") {
|
||||
return res.status(403).json({ message: "Admin access required" });
|
||||
}
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Error checking admin status" });
|
||||
}
|
||||
};
|
||||
|
||||
// Auth routes
|
||||
app.get('/api/auth/user', isAuthenticated, async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.claims.sub;
|
||||
const user = await storage.getUser(userId);
|
||||
res.json(user);
|
||||
} catch (error) {
|
||||
console.error("Error fetching user:", error);
|
||||
res.status(500).json({ message: "Failed to fetch user" });
|
||||
}
|
||||
});
|
||||
|
||||
// User management routes
|
||||
app.get('/api/users', isAuthenticated, isAdmin, async (req, res) => {
|
||||
try {
|
||||
const users = await storage.getAllDJs();
|
||||
res.json(users);
|
||||
} catch (error) {
|
||||
console.error("Error fetching users:", error);
|
||||
res.status(500).json({ message: "Failed to fetch users" });
|
||||
}
|
||||
});
|
||||
|
||||
app.patch('/api/users/:id', isAuthenticated, async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.claims.sub;
|
||||
const targetUserId = req.params.id;
|
||||
const user = await storage.getUser(userId);
|
||||
|
||||
// Allow users to update their own profile or admins to update any profile
|
||||
if (userId !== targetUserId && user?.role !== "admin") {
|
||||
return res.status(403).json({ message: "Permission denied" });
|
||||
}
|
||||
|
||||
const updates = updateUserSchema.parse(req.body);
|
||||
const updatedUser = await storage.updateUser(targetUserId, updates);
|
||||
res.json(updatedUser);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ message: fromZodError(error).toString() });
|
||||
}
|
||||
console.error("Error updating user:", error);
|
||||
res.status(500).json({ message: "Failed to update user" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/users/:id/deactivate', isAuthenticated, isAdmin, async (req, res) => {
|
||||
try {
|
||||
await storage.deactivateUser(req.params.id);
|
||||
res.json({ message: "User deactivated successfully" });
|
||||
} catch (error) {
|
||||
console.error("Error deactivating user:", error);
|
||||
res.status(500).json({ message: "Failed to deactivate user" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/users/:id/reactivate', isAuthenticated, isAdmin, async (req, res) => {
|
||||
try {
|
||||
await storage.reactivateUser(req.params.id);
|
||||
res.json({ message: "User reactivated successfully" });
|
||||
} catch (error) {
|
||||
console.error("Error reactivating user:", error);
|
||||
res.status(500).json({ message: "Failed to reactivate user" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/users/:id/make-admin', isAuthenticated, isAdmin, async (req, res) => {
|
||||
try {
|
||||
await storage.makeAdmin(req.params.id);
|
||||
res.json({ message: "User promoted to admin successfully" });
|
||||
} catch (error) {
|
||||
console.error("Error promoting user to admin:", error);
|
||||
res.status(500).json({ message: "Failed to promote user to admin" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/users/:id/remove-admin', isAuthenticated, isAdmin, async (req, res) => {
|
||||
try {
|
||||
await storage.removeAdmin(req.params.id);
|
||||
res.json({ message: "Admin privileges removed successfully" });
|
||||
} catch (error) {
|
||||
console.error("Error removing admin privileges:", error);
|
||||
res.status(500).json({ message: "Failed to remove admin privileges" });
|
||||
}
|
||||
});
|
||||
|
||||
// Event routes
|
||||
app.get('/api/events', isAuthenticated, async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.claims.sub;
|
||||
const user = await storage.getUser(userId);
|
||||
|
||||
if (user?.role === "admin") {
|
||||
const events = await storage.getAllEvents();
|
||||
res.json(events);
|
||||
} else {
|
||||
const events = await storage.getEventsByDJ(userId);
|
||||
res.json(events);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching events:", error);
|
||||
res.status(500).json({ message: "Failed to fetch events" });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/events/upcoming', isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const limit = req.query.limit ? parseInt(req.query.limit as string) : 50;
|
||||
const events = await storage.getUpcomingEvents(limit);
|
||||
res.json(events);
|
||||
} catch (error) {
|
||||
console.error("Error fetching upcoming events:", error);
|
||||
res.status(500).json({ message: "Failed to fetch upcoming events" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/events', isAuthenticated, async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.claims.sub;
|
||||
const eventData = insertEventSchema.parse({ ...req.body, djId: userId });
|
||||
const event = await storage.createEvent(eventData);
|
||||
res.json(event);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ message: fromZodError(error).toString() });
|
||||
}
|
||||
console.error("Error creating event:", error);
|
||||
res.status(500).json({ message: "Failed to create event" });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/events/:id', isAuthenticated, async (req: any, res) => {
|
||||
try {
|
||||
const event = await storage.getEvent(parseInt(req.params.id));
|
||||
if (!event) {
|
||||
return res.status(404).json({ message: "Event not found" });
|
||||
}
|
||||
|
||||
const userId = req.user.claims.sub;
|
||||
const user = await storage.getUser(userId);
|
||||
|
||||
// Allow access to event if user is admin or the DJ assigned to the event
|
||||
if (user?.role !== "admin" && event.djId !== userId) {
|
||||
return res.status(403).json({ message: "Permission denied" });
|
||||
}
|
||||
|
||||
res.json(event);
|
||||
} catch (error) {
|
||||
console.error("Error fetching event:", error);
|
||||
res.status(500).json({ message: "Failed to fetch event" });
|
||||
}
|
||||
});
|
||||
|
||||
app.patch('/api/events/:id', isAuthenticated, async (req: any, res) => {
|
||||
try {
|
||||
const eventId = parseInt(req.params.id);
|
||||
const event = await storage.getEvent(eventId);
|
||||
if (!event) {
|
||||
return res.status(404).json({ message: "Event not found" });
|
||||
}
|
||||
|
||||
const userId = req.user.claims.sub;
|
||||
const user = await storage.getUser(userId);
|
||||
|
||||
// Only allow DJ to edit their own self-added events or admin to edit any event
|
||||
if (user?.role !== "admin" && (event.djId !== userId || event.isAssignedByAdmin)) {
|
||||
return res.status(403).json({ message: "Permission denied" });
|
||||
}
|
||||
|
||||
const updates = updateEventSchema.parse(req.body);
|
||||
const updatedEvent = await storage.updateEvent(eventId, updates);
|
||||
res.json(updatedEvent);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ message: fromZodError(error).toString() });
|
||||
}
|
||||
console.error("Error updating event:", error);
|
||||
res.status(500).json({ message: "Failed to update event" });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/events/:id', isAuthenticated, async (req: any, res) => {
|
||||
try {
|
||||
const eventId = parseInt(req.params.id);
|
||||
const event = await storage.getEvent(eventId);
|
||||
if (!event) {
|
||||
return res.status(404).json({ message: "Event not found" });
|
||||
}
|
||||
|
||||
const userId = req.user.claims.sub;
|
||||
const user = await storage.getUser(userId);
|
||||
|
||||
// Only allow DJ to delete their own self-added events or admin to delete any event
|
||||
if (user?.role !== "admin" && (event.djId !== userId || event.isAssignedByAdmin)) {
|
||||
return res.status(403).json({ message: "Permission denied" });
|
||||
}
|
||||
|
||||
await storage.deleteEvent(eventId);
|
||||
res.json({ message: "Event deleted successfully" });
|
||||
} catch (error) {
|
||||
console.error("Error deleting event:", error);
|
||||
res.status(500).json({ message: "Failed to delete event" });
|
||||
}
|
||||
});
|
||||
|
||||
// Public API for WordPress integration
|
||||
app.get('/api/public/upcoming-events', async (req, res) => {
|
||||
try {
|
||||
const limit = req.query.limit ? parseInt(req.query.limit as string) : 10;
|
||||
const events = await storage.getUpcomingEvents(limit);
|
||||
|
||||
// Format events for WordPress widget
|
||||
const formattedEvents = events.map(event => ({
|
||||
id: event.id,
|
||||
name: event.name,
|
||||
date: event.date,
|
||||
startTime: event.startTime,
|
||||
endTime: event.endTime,
|
||||
locationName: event.locationName,
|
||||
locationAddress: event.locationAddress,
|
||||
djId: event.djId,
|
||||
eventTypeId: event.eventTypeId,
|
||||
description: event.description,
|
||||
}));
|
||||
|
||||
res.json(formattedEvents);
|
||||
} catch (error) {
|
||||
console.error("Error fetching public events:", error);
|
||||
res.status(500).json({ message: "Failed to fetch events" });
|
||||
}
|
||||
});
|
||||
|
||||
// Event type routes
|
||||
app.get('/api/event-types', isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const eventTypes = await storage.getAllEventTypes();
|
||||
res.json(eventTypes);
|
||||
} catch (error) {
|
||||
console.error("Error fetching event types:", error);
|
||||
res.status(500).json({ message: "Failed to fetch event types" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/event-types', isAuthenticated, isAdmin, async (req, res) => {
|
||||
try {
|
||||
const eventTypeData = insertEventTypeSchema.parse(req.body);
|
||||
const eventType = await storage.createEventType(eventTypeData);
|
||||
res.json(eventType);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ message: fromZodError(error).toString() });
|
||||
}
|
||||
console.error("Error creating event type:", error);
|
||||
res.status(500).json({ message: "Failed to create event type" });
|
||||
}
|
||||
});
|
||||
|
||||
app.patch('/api/event-types/:id', isAuthenticated, isAdmin, async (req, res) => {
|
||||
try {
|
||||
const eventTypeId = parseInt(req.params.id);
|
||||
const updates = updateEventTypeSchema.parse(req.body);
|
||||
const eventType = await storage.updateEventType(eventTypeId, updates);
|
||||
res.json(eventType);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ message: fromZodError(error).toString() });
|
||||
}
|
||||
console.error("Error updating event type:", error);
|
||||
res.status(500).json({ message: "Failed to update event type" });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/event-types/:id', isAuthenticated, isAdmin, async (req, res) => {
|
||||
try {
|
||||
const eventTypeId = parseInt(req.params.id);
|
||||
await storage.deleteEventType(eventTypeId);
|
||||
res.json({ message: "Event type deleted successfully" });
|
||||
} catch (error) {
|
||||
console.error("Error deleting event type:", error);
|
||||
res.status(500).json({ message: "Failed to delete event type" });
|
||||
}
|
||||
});
|
||||
|
||||
// Schedule template routes
|
||||
app.get('/api/schedule-templates', isAuthenticated, isAdmin, async (req, res) => {
|
||||
try {
|
||||
const templates = await storage.getAllScheduleTemplates();
|
||||
res.json(templates);
|
||||
} catch (error) {
|
||||
console.error("Error fetching schedule templates:", error);
|
||||
res.status(500).json({ message: "Failed to fetch schedule templates" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/schedule-templates', isAuthenticated, isAdmin, async (req, res) => {
|
||||
try {
|
||||
const templateData = insertScheduleTemplateSchema.parse(req.body);
|
||||
const template = await storage.createScheduleTemplate(templateData);
|
||||
res.json(template);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ message: fromZodError(error).toString() });
|
||||
}
|
||||
console.error("Error creating schedule template:", error);
|
||||
res.status(500).json({ message: "Failed to create schedule template" });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/schedule-templates/:id/slots', isAuthenticated, isAdmin, async (req, res) => {
|
||||
try {
|
||||
const templateId = parseInt(req.params.id);
|
||||
const slots = await storage.getScheduleTemplateSlots(templateId);
|
||||
res.json(slots);
|
||||
} catch (error) {
|
||||
console.error("Error fetching template slots:", error);
|
||||
res.status(500).json({ message: "Failed to fetch template slots" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/schedule-templates/:id/slots', isAuthenticated, isAdmin, async (req, res) => {
|
||||
try {
|
||||
const templateId = parseInt(req.params.id);
|
||||
const slotData = insertScheduleTemplateSlotSchema.parse({ ...req.body, templateId });
|
||||
const slot = await storage.createScheduleTemplateSlot(slotData);
|
||||
res.json(slot);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ message: fromZodError(error).toString() });
|
||||
}
|
||||
console.error("Error creating template slot:", error);
|
||||
res.status(500).json({ message: "Failed to create template slot" });
|
||||
}
|
||||
});
|
||||
|
||||
// Social links routes
|
||||
app.get('/api/social-links', isAuthenticated, async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.claims.sub;
|
||||
const links = await storage.getSocialLinksByDJ(userId);
|
||||
res.json(links);
|
||||
} catch (error) {
|
||||
console.error("Error fetching social links:", error);
|
||||
res.status(500).json({ message: "Failed to fetch social links" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/social-links', isAuthenticated, async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.claims.sub;
|
||||
const linkData = insertSocialLinkSchema.parse({ ...req.body, djId: userId });
|
||||
const link = await storage.createSocialLink(linkData);
|
||||
res.json(link);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ message: fromZodError(error).toString() });
|
||||
}
|
||||
console.error("Error creating social link:", error);
|
||||
res.status(500).json({ message: "Failed to create social link" });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/social-links/:id', isAuthenticated, async (req: any, res) => {
|
||||
try {
|
||||
await storage.deleteSocialLink(parseInt(req.params.id));
|
||||
res.json({ message: "Social link deleted successfully" });
|
||||
} catch (error) {
|
||||
console.error("Error deleting social link:", error);
|
||||
res.status(500).json({ message: "Failed to delete social link" });
|
||||
}
|
||||
});
|
||||
|
||||
// Availability routes
|
||||
app.get('/api/availability', isAuthenticated, async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.claims.sub;
|
||||
const availability = await storage.getAvailabilityByDJ(userId);
|
||||
res.json(availability);
|
||||
} catch (error) {
|
||||
console.error("Error fetching availability:", error);
|
||||
res.status(500).json({ message: "Failed to fetch availability" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/availability', isAuthenticated, async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.claims.sub;
|
||||
const availabilityData = insertAvailabilitySchema.parse({ ...req.body, djId: userId });
|
||||
const availability = await storage.createAvailability(availabilityData);
|
||||
res.json(availability);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ message: fromZodError(error).toString() });
|
||||
}
|
||||
console.error("Error creating availability:", error);
|
||||
res.status(500).json({ message: "Failed to create availability" });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/availability/:id', isAuthenticated, async (req: any, res) => {
|
||||
try {
|
||||
await storage.deleteAvailability(parseInt(req.params.id));
|
||||
res.json({ message: "Availability deleted successfully" });
|
||||
} catch (error) {
|
||||
console.error("Error deleting availability:", error);
|
||||
res.status(500).json({ message: "Failed to delete availability" });
|
||||
}
|
||||
});
|
||||
|
||||
// Removal request routes
|
||||
app.get('/api/removal-requests', isAuthenticated, async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.claims.sub;
|
||||
const user = await storage.getUser(userId);
|
||||
|
||||
if (user?.role === "admin") {
|
||||
const requests = await storage.getPendingRemovalRequests();
|
||||
res.json(requests);
|
||||
} else {
|
||||
return res.status(403).json({ message: "Admin access required" });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching removal requests:", error);
|
||||
res.status(500).json({ message: "Failed to fetch removal requests" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/removal-requests', isAuthenticated, async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.claims.sub;
|
||||
const requestData = insertRemovalRequestSchema.parse({ ...req.body, djId: userId });
|
||||
const request = await storage.createRemovalRequest(requestData);
|
||||
res.json(request);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ message: fromZodError(error).toString() });
|
||||
}
|
||||
console.error("Error creating removal request:", error);
|
||||
res.status(500).json({ message: "Failed to create removal request" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/removal-requests/:id/approve', isAuthenticated, isAdmin, async (req: any, res) => {
|
||||
try {
|
||||
const requestId = parseInt(req.params.id);
|
||||
const reviewedBy = req.user.claims.sub;
|
||||
await storage.approveRemovalRequest(requestId, reviewedBy);
|
||||
res.json({ message: "Removal request approved successfully" });
|
||||
} catch (error) {
|
||||
console.error("Error approving removal request:", error);
|
||||
res.status(500).json({ message: "Failed to approve removal request" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/removal-requests/:id/deny', isAuthenticated, isAdmin, async (req: any, res) => {
|
||||
try {
|
||||
const requestId = parseInt(req.params.id);
|
||||
const reviewedBy = req.user.claims.sub;
|
||||
await storage.denyRemovalRequest(requestId, reviewedBy);
|
||||
res.json({ message: "Removal request denied successfully" });
|
||||
} catch (error) {
|
||||
console.error("Error denying removal request:", error);
|
||||
res.status(500).json({ message: "Failed to deny removal request" });
|
||||
}
|
||||
});
|
||||
|
||||
// Invitation routes
|
||||
app.post('/api/invitations', isAuthenticated, isAdmin, async (req: any, res) => {
|
||||
try {
|
||||
const createdBy = req.user.claims.sub;
|
||||
const token = crypto.randomBytes(32).toString('hex');
|
||||
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
|
||||
|
||||
const invitationData = insertInvitationSchema.parse({
|
||||
...req.body,
|
||||
token,
|
||||
createdBy,
|
||||
expiresAt
|
||||
});
|
||||
|
||||
const invitation = await storage.createInvitation(invitationData);
|
||||
res.json(invitation);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ message: fromZodError(error).toString() });
|
||||
}
|
||||
console.error("Error creating invitation:", error);
|
||||
res.status(500).json({ message: "Failed to create invitation" });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/invitations', isAuthenticated, isAdmin, async (req, res) => {
|
||||
try {
|
||||
const invitations = await storage.getActiveInvitations();
|
||||
res.json(invitations);
|
||||
} catch (error) {
|
||||
console.error("Error fetching invitations:", error);
|
||||
res.status(500).json({ message: "Failed to fetch invitations" });
|
||||
}
|
||||
});
|
||||
|
||||
// Statistics routes
|
||||
app.get('/api/stats/dashboard', isAuthenticated, async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.claims.sub;
|
||||
const stats = await storage.getDashboardStats(userId);
|
||||
res.json(stats);
|
||||
} catch (error) {
|
||||
console.error("Error fetching dashboard stats:", error);
|
||||
res.status(500).json({ message: "Failed to fetch dashboard stats" });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/stats/admin', isAuthenticated, isAdmin, async (req, res) => {
|
||||
try {
|
||||
const stats = await storage.getAdminStats();
|
||||
res.json(stats);
|
||||
} catch (error) {
|
||||
console.error("Error fetching admin stats:", error);
|
||||
res.status(500).json({ message: "Failed to fetch admin stats" });
|
||||
}
|
||||
});
|
||||
|
||||
const httpServer = createServer(app);
|
||||
return httpServer;
|
||||
}
|
||||
@@ -0,0 +1,558 @@
|
||||
import {
|
||||
users,
|
||||
events,
|
||||
eventTypes,
|
||||
scheduleTemplates,
|
||||
scheduleTemplateSlots,
|
||||
socialLinks,
|
||||
availability,
|
||||
slotEligibility,
|
||||
removalRequests,
|
||||
invitations,
|
||||
type User,
|
||||
type UpsertUser,
|
||||
type InsertUser,
|
||||
type UpdateUser,
|
||||
type Event,
|
||||
type InsertEvent,
|
||||
type UpdateEvent,
|
||||
type EventType,
|
||||
type InsertEventType,
|
||||
type UpdateEventType,
|
||||
type ScheduleTemplate,
|
||||
type InsertScheduleTemplate,
|
||||
type UpdateScheduleTemplate,
|
||||
type ScheduleTemplateSlot,
|
||||
type InsertScheduleTemplateSlot,
|
||||
type SocialLink,
|
||||
type InsertSocialLink,
|
||||
type Availability,
|
||||
type InsertAvailability,
|
||||
type SlotEligibility,
|
||||
type InsertSlotEligibility,
|
||||
type RemovalRequest,
|
||||
type InsertRemovalRequest,
|
||||
type UpdateRemovalRequest,
|
||||
type Invitation,
|
||||
type InsertInvitation,
|
||||
} from "@shared/schema";
|
||||
import { db } from "./db";
|
||||
import { eq, and, gte, lte, desc, asc, sql, inArray } from "drizzle-orm";
|
||||
|
||||
export interface IStorage {
|
||||
// User operations (IMPORTANT: mandatory for Replit Auth)
|
||||
getUser(id: string): Promise<User | undefined>;
|
||||
upsertUser(user: UpsertUser): Promise<User>;
|
||||
updateUser(id: string, updates: UpdateUser): Promise<User>;
|
||||
getAllDJs(): Promise<User[]>;
|
||||
deactivateUser(id: string): Promise<void>;
|
||||
reactivateUser(id: string): Promise<void>;
|
||||
makeAdmin(id: string): Promise<void>;
|
||||
removeAdmin(id: string): Promise<void>;
|
||||
|
||||
// Event operations
|
||||
createEvent(event: InsertEvent): Promise<Event>;
|
||||
getEvent(id: number): Promise<Event | undefined>;
|
||||
getEventsByDJ(djId: string): Promise<Event[]>;
|
||||
getUpcomingEvents(limit?: number): Promise<Event[]>;
|
||||
getAllEvents(): Promise<Event[]>;
|
||||
updateEvent(id: number, updates: UpdateEvent): Promise<Event>;
|
||||
deleteEvent(id: number): Promise<void>;
|
||||
getEventsByDateRange(startDate: string, endDate: string): Promise<Event[]>;
|
||||
getEventsByMonth(year: number, month: number): Promise<Event[]>;
|
||||
|
||||
// Event type operations
|
||||
createEventType(eventType: InsertEventType): Promise<EventType>;
|
||||
getEventType(id: number): Promise<EventType | undefined>;
|
||||
getAllEventTypes(): Promise<EventType[]>;
|
||||
updateEventType(id: number, updates: UpdateEventType): Promise<EventType>;
|
||||
deleteEventType(id: number): Promise<void>;
|
||||
|
||||
// Schedule template operations
|
||||
createScheduleTemplate(template: InsertScheduleTemplate): Promise<ScheduleTemplate>;
|
||||
getScheduleTemplate(id: number): Promise<ScheduleTemplate | undefined>;
|
||||
getAllScheduleTemplates(): Promise<ScheduleTemplate[]>;
|
||||
updateScheduleTemplate(id: number, updates: UpdateScheduleTemplate): Promise<ScheduleTemplate>;
|
||||
deleteScheduleTemplate(id: number): Promise<void>;
|
||||
|
||||
// Schedule template slot operations
|
||||
createScheduleTemplateSlot(slot: InsertScheduleTemplateSlot): Promise<ScheduleTemplateSlot>;
|
||||
getScheduleTemplateSlots(templateId: number): Promise<ScheduleTemplateSlot[]>;
|
||||
deleteScheduleTemplateSlot(id: number): Promise<void>;
|
||||
|
||||
// Social link operations
|
||||
createSocialLink(link: InsertSocialLink): Promise<SocialLink>;
|
||||
getSocialLinksByDJ(djId: string): Promise<SocialLink[]>;
|
||||
updateSocialLink(id: number, updates: Partial<InsertSocialLink>): Promise<SocialLink>;
|
||||
deleteSocialLink(id: number): Promise<void>;
|
||||
|
||||
// Availability operations
|
||||
createAvailability(availability: InsertAvailability): Promise<Availability>;
|
||||
getAvailabilityByDJ(djId: string): Promise<Availability[]>;
|
||||
deleteAvailability(id: number): Promise<void>;
|
||||
isAvailable(djId: string, date: string): Promise<boolean>;
|
||||
|
||||
// Slot eligibility operations
|
||||
createSlotEligibility(eligibility: InsertSlotEligibility): Promise<SlotEligibility>;
|
||||
getSlotEligibilityByDJ(djId: string): Promise<SlotEligibility[]>;
|
||||
updateSlotEligibility(id: number, updates: Partial<InsertSlotEligibility>): Promise<SlotEligibility>;
|
||||
deleteSlotEligibility(id: number): Promise<void>;
|
||||
|
||||
// Removal request operations
|
||||
createRemovalRequest(request: InsertRemovalRequest): Promise<RemovalRequest>;
|
||||
getRemovalRequest(id: number): Promise<RemovalRequest | undefined>;
|
||||
getPendingRemovalRequests(): Promise<RemovalRequest[]>;
|
||||
updateRemovalRequest(id: number, updates: UpdateRemovalRequest): Promise<RemovalRequest>;
|
||||
approveRemovalRequest(id: number, reviewedBy: string): Promise<void>;
|
||||
denyRemovalRequest(id: number, reviewedBy: string): Promise<void>;
|
||||
|
||||
// Invitation operations
|
||||
createInvitation(invitation: InsertInvitation): Promise<Invitation>;
|
||||
getInvitationByToken(token: string): Promise<Invitation | undefined>;
|
||||
markInvitationAsUsed(id: number): Promise<void>;
|
||||
getActiveInvitations(): Promise<Invitation[]>;
|
||||
|
||||
// Statistics
|
||||
getDashboardStats(djId: string): Promise<{
|
||||
upcomingEvents: number;
|
||||
thisMonth: number;
|
||||
pendingRequests: number;
|
||||
totalEvents: number;
|
||||
}>;
|
||||
getAdminStats(): Promise<{
|
||||
totalDJs: number;
|
||||
activeEvents: number;
|
||||
pendingRequests: number;
|
||||
thisMonth: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export class DatabaseStorage implements IStorage {
|
||||
// User operations
|
||||
async getUser(id: string): Promise<User | undefined> {
|
||||
const [user] = await db.select().from(users).where(eq(users.id, id));
|
||||
return user;
|
||||
}
|
||||
|
||||
async upsertUser(userData: UpsertUser): Promise<User> {
|
||||
const [user] = await db
|
||||
.insert(users)
|
||||
.values(userData)
|
||||
.onConflictDoUpdate({
|
||||
target: users.id,
|
||||
set: {
|
||||
...userData,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
})
|
||||
.returning();
|
||||
return user;
|
||||
}
|
||||
|
||||
async updateUser(id: string, updates: UpdateUser): Promise<User> {
|
||||
const [user] = await db
|
||||
.update(users)
|
||||
.set({ ...updates, updatedAt: new Date() })
|
||||
.where(eq(users.id, id))
|
||||
.returning();
|
||||
return user;
|
||||
}
|
||||
|
||||
async getAllDJs(): Promise<User[]> {
|
||||
return await db.select().from(users).where(eq(users.role, "dj")).orderBy(asc(users.displayName));
|
||||
}
|
||||
|
||||
async deactivateUser(id: string): Promise<void> {
|
||||
await db.update(users).set({ isActive: false, updatedAt: new Date() }).where(eq(users.id, id));
|
||||
}
|
||||
|
||||
async reactivateUser(id: string): Promise<void> {
|
||||
await db.update(users).set({ isActive: true, updatedAt: new Date() }).where(eq(users.id, id));
|
||||
}
|
||||
|
||||
async makeAdmin(id: string): Promise<void> {
|
||||
await db.update(users).set({ role: "admin", updatedAt: new Date() }).where(eq(users.id, id));
|
||||
}
|
||||
|
||||
async removeAdmin(id: string): Promise<void> {
|
||||
await db.update(users).set({ role: "dj", updatedAt: new Date() }).where(eq(users.id, id));
|
||||
}
|
||||
|
||||
// Event operations
|
||||
async createEvent(event: InsertEvent): Promise<Event> {
|
||||
const [newEvent] = await db.insert(events).values(event).returning();
|
||||
return newEvent;
|
||||
}
|
||||
|
||||
async getEvent(id: number): Promise<Event | undefined> {
|
||||
const [event] = await db.select().from(events).where(eq(events.id, id));
|
||||
return event;
|
||||
}
|
||||
|
||||
async getEventsByDJ(djId: string): Promise<Event[]> {
|
||||
return await db
|
||||
.select()
|
||||
.from(events)
|
||||
.where(eq(events.djId, djId))
|
||||
.orderBy(asc(events.date), asc(events.startTime));
|
||||
}
|
||||
|
||||
async getUpcomingEvents(limit = 50): Promise<Event[]> {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
return await db
|
||||
.select()
|
||||
.from(events)
|
||||
.where(gte(events.date, today))
|
||||
.orderBy(asc(events.date), asc(events.startTime))
|
||||
.limit(limit);
|
||||
}
|
||||
|
||||
async getAllEvents(): Promise<Event[]> {
|
||||
return await db
|
||||
.select()
|
||||
.from(events)
|
||||
.orderBy(desc(events.date), desc(events.startTime));
|
||||
}
|
||||
|
||||
async updateEvent(id: number, updates: UpdateEvent): Promise<Event> {
|
||||
const [event] = await db
|
||||
.update(events)
|
||||
.set({ ...updates, updatedAt: new Date() })
|
||||
.where(eq(events.id, id))
|
||||
.returning();
|
||||
return event;
|
||||
}
|
||||
|
||||
async deleteEvent(id: number): Promise<void> {
|
||||
await db.delete(events).where(eq(events.id, id));
|
||||
}
|
||||
|
||||
async getEventsByDateRange(startDate: string, endDate: string): Promise<Event[]> {
|
||||
return await db
|
||||
.select()
|
||||
.from(events)
|
||||
.where(and(gte(events.date, startDate), lte(events.date, endDate)))
|
||||
.orderBy(asc(events.date), asc(events.startTime));
|
||||
}
|
||||
|
||||
async getEventsByMonth(year: number, month: number): Promise<Event[]> {
|
||||
const startDate = `${year}-${month.toString().padStart(2, '0')}-01`;
|
||||
const endDate = `${year}-${month.toString().padStart(2, '0')}-31`;
|
||||
return await this.getEventsByDateRange(startDate, endDate);
|
||||
}
|
||||
|
||||
// Event type operations
|
||||
async createEventType(eventType: InsertEventType): Promise<EventType> {
|
||||
const [newEventType] = await db.insert(eventTypes).values(eventType).returning();
|
||||
return newEventType;
|
||||
}
|
||||
|
||||
async getEventType(id: number): Promise<EventType | undefined> {
|
||||
const [eventType] = await db.select().from(eventTypes).where(eq(eventTypes.id, id));
|
||||
return eventType;
|
||||
}
|
||||
|
||||
async getAllEventTypes(): Promise<EventType[]> {
|
||||
return await db.select().from(eventTypes).where(eq(eventTypes.isActive, true)).orderBy(asc(eventTypes.name));
|
||||
}
|
||||
|
||||
async updateEventType(id: number, updates: UpdateEventType): Promise<EventType> {
|
||||
const [eventType] = await db
|
||||
.update(eventTypes)
|
||||
.set(updates)
|
||||
.where(eq(eventTypes.id, id))
|
||||
.returning();
|
||||
return eventType;
|
||||
}
|
||||
|
||||
async deleteEventType(id: number): Promise<void> {
|
||||
await db.update(eventTypes).set({ isActive: false }).where(eq(eventTypes.id, id));
|
||||
}
|
||||
|
||||
// Schedule template operations
|
||||
async createScheduleTemplate(template: InsertScheduleTemplate): Promise<ScheduleTemplate> {
|
||||
const [newTemplate] = await db.insert(scheduleTemplates).values(template).returning();
|
||||
return newTemplate;
|
||||
}
|
||||
|
||||
async getScheduleTemplate(id: number): Promise<ScheduleTemplate | undefined> {
|
||||
const [template] = await db.select().from(scheduleTemplates).where(eq(scheduleTemplates.id, id));
|
||||
return template;
|
||||
}
|
||||
|
||||
async getAllScheduleTemplates(): Promise<ScheduleTemplate[]> {
|
||||
return await db.select().from(scheduleTemplates).where(eq(scheduleTemplates.isActive, true)).orderBy(asc(scheduleTemplates.name));
|
||||
}
|
||||
|
||||
async updateScheduleTemplate(id: number, updates: UpdateScheduleTemplate): Promise<ScheduleTemplate> {
|
||||
const [template] = await db
|
||||
.update(scheduleTemplates)
|
||||
.set(updates)
|
||||
.where(eq(scheduleTemplates.id, id))
|
||||
.returning();
|
||||
return template;
|
||||
}
|
||||
|
||||
async deleteScheduleTemplate(id: number): Promise<void> {
|
||||
await db.update(scheduleTemplates).set({ isActive: false }).where(eq(scheduleTemplates.id, id));
|
||||
}
|
||||
|
||||
// Schedule template slot operations
|
||||
async createScheduleTemplateSlot(slot: InsertScheduleTemplateSlot): Promise<ScheduleTemplateSlot> {
|
||||
const [newSlot] = await db.insert(scheduleTemplateSlots).values(slot).returning();
|
||||
return newSlot;
|
||||
}
|
||||
|
||||
async getScheduleTemplateSlots(templateId: number): Promise<ScheduleTemplateSlot[]> {
|
||||
return await db
|
||||
.select()
|
||||
.from(scheduleTemplateSlots)
|
||||
.where(eq(scheduleTemplateSlots.templateId, templateId))
|
||||
.orderBy(asc(scheduleTemplateSlots.dayOfWeek), asc(scheduleTemplateSlots.startTime));
|
||||
}
|
||||
|
||||
async deleteScheduleTemplateSlot(id: number): Promise<void> {
|
||||
await db.delete(scheduleTemplateSlots).where(eq(scheduleTemplateSlots.id, id));
|
||||
}
|
||||
|
||||
// Social link operations
|
||||
async createSocialLink(link: InsertSocialLink): Promise<SocialLink> {
|
||||
const [newLink] = await db.insert(socialLinks).values(link).returning();
|
||||
return newLink;
|
||||
}
|
||||
|
||||
async getSocialLinksByDJ(djId: string): Promise<SocialLink[]> {
|
||||
return await db.select().from(socialLinks).where(eq(socialLinks.djId, djId));
|
||||
}
|
||||
|
||||
async updateSocialLink(id: number, updates: Partial<InsertSocialLink>): Promise<SocialLink> {
|
||||
const [link] = await db
|
||||
.update(socialLinks)
|
||||
.set(updates)
|
||||
.where(eq(socialLinks.id, id))
|
||||
.returning();
|
||||
return link;
|
||||
}
|
||||
|
||||
async deleteSocialLink(id: number): Promise<void> {
|
||||
await db.delete(socialLinks).where(eq(socialLinks.id, id));
|
||||
}
|
||||
|
||||
// Availability operations
|
||||
async createAvailability(availabilityData: InsertAvailability): Promise<Availability> {
|
||||
const [newAvailability] = await db.insert(availability).values(availabilityData).returning();
|
||||
return newAvailability;
|
||||
}
|
||||
|
||||
async getAvailabilityByDJ(djId: string): Promise<Availability[]> {
|
||||
return await db
|
||||
.select()
|
||||
.from(availability)
|
||||
.where(eq(availability.djId, djId))
|
||||
.orderBy(asc(availability.startDate));
|
||||
}
|
||||
|
||||
async deleteAvailability(id: number): Promise<void> {
|
||||
await db.delete(availability).where(eq(availability.id, id));
|
||||
}
|
||||
|
||||
async isAvailable(djId: string, date: string): Promise<boolean> {
|
||||
const [unavailable] = await db
|
||||
.select()
|
||||
.from(availability)
|
||||
.where(
|
||||
and(
|
||||
eq(availability.djId, djId),
|
||||
lte(availability.startDate, date),
|
||||
gte(availability.endDate, date)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
return !unavailable;
|
||||
}
|
||||
|
||||
// Slot eligibility operations
|
||||
async createSlotEligibility(eligibility: InsertSlotEligibility): Promise<SlotEligibility> {
|
||||
const [newEligibility] = await db.insert(slotEligibility).values(eligibility).returning();
|
||||
return newEligibility;
|
||||
}
|
||||
|
||||
async getSlotEligibilityByDJ(djId: string): Promise<SlotEligibility[]> {
|
||||
return await db.select().from(slotEligibility).where(eq(slotEligibility.djId, djId));
|
||||
}
|
||||
|
||||
async updateSlotEligibility(id: number, updates: Partial<InsertSlotEligibility>): Promise<SlotEligibility> {
|
||||
const [eligibility] = await db
|
||||
.update(slotEligibility)
|
||||
.set(updates)
|
||||
.where(eq(slotEligibility.id, id))
|
||||
.returning();
|
||||
return eligibility;
|
||||
}
|
||||
|
||||
async deleteSlotEligibility(id: number): Promise<void> {
|
||||
await db.delete(slotEligibility).where(eq(slotEligibility.id, id));
|
||||
}
|
||||
|
||||
// Removal request operations
|
||||
async createRemovalRequest(request: InsertRemovalRequest): Promise<RemovalRequest> {
|
||||
const [newRequest] = await db.insert(removalRequests).values(request).returning();
|
||||
return newRequest;
|
||||
}
|
||||
|
||||
async getRemovalRequest(id: number): Promise<RemovalRequest | undefined> {
|
||||
const [request] = await db.select().from(removalRequests).where(eq(removalRequests.id, id));
|
||||
return request;
|
||||
}
|
||||
|
||||
async getPendingRemovalRequests(): Promise<RemovalRequest[]> {
|
||||
return await db
|
||||
.select()
|
||||
.from(removalRequests)
|
||||
.where(eq(removalRequests.status, "pending"))
|
||||
.orderBy(asc(removalRequests.createdAt));
|
||||
}
|
||||
|
||||
async updateRemovalRequest(id: number, updates: UpdateRemovalRequest): Promise<RemovalRequest> {
|
||||
const [request] = await db
|
||||
.update(removalRequests)
|
||||
.set(updates)
|
||||
.where(eq(removalRequests.id, id))
|
||||
.returning();
|
||||
return request;
|
||||
}
|
||||
|
||||
async approveRemovalRequest(id: number, reviewedBy: string): Promise<void> {
|
||||
await db
|
||||
.update(removalRequests)
|
||||
.set({
|
||||
status: "approved",
|
||||
reviewedBy,
|
||||
reviewedAt: new Date(),
|
||||
})
|
||||
.where(eq(removalRequests.id, id));
|
||||
}
|
||||
|
||||
async denyRemovalRequest(id: number, reviewedBy: string): Promise<void> {
|
||||
await db
|
||||
.update(removalRequests)
|
||||
.set({
|
||||
status: "denied",
|
||||
reviewedBy,
|
||||
reviewedAt: new Date(),
|
||||
})
|
||||
.where(eq(removalRequests.id, id));
|
||||
}
|
||||
|
||||
// Invitation operations
|
||||
async createInvitation(invitation: InsertInvitation): Promise<Invitation> {
|
||||
const [newInvitation] = await db.insert(invitations).values(invitation).returning();
|
||||
return newInvitation;
|
||||
}
|
||||
|
||||
async getInvitationByToken(token: string): Promise<Invitation | undefined> {
|
||||
const [invitation] = await db.select().from(invitations).where(eq(invitations.token, token));
|
||||
return invitation;
|
||||
}
|
||||
|
||||
async markInvitationAsUsed(id: number): Promise<void> {
|
||||
await db.update(invitations).set({ isUsed: true }).where(eq(invitations.id, id));
|
||||
}
|
||||
|
||||
async getActiveInvitations(): Promise<Invitation[]> {
|
||||
return await db
|
||||
.select()
|
||||
.from(invitations)
|
||||
.where(and(eq(invitations.isUsed, false), gte(invitations.expiresAt, new Date())))
|
||||
.orderBy(desc(invitations.createdAt));
|
||||
}
|
||||
|
||||
// Statistics
|
||||
async getDashboardStats(djId: string): Promise<{
|
||||
upcomingEvents: number;
|
||||
thisMonth: number;
|
||||
pendingRequests: number;
|
||||
totalEvents: number;
|
||||
}> {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const currentMonth = new Date().getMonth() + 1;
|
||||
const currentYear = new Date().getFullYear();
|
||||
const monthStart = `${currentYear}-${currentMonth.toString().padStart(2, '0')}-01`;
|
||||
const monthEnd = `${currentYear}-${currentMonth.toString().padStart(2, '0')}-31`;
|
||||
|
||||
const [upcomingEvents] = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(events)
|
||||
.where(and(eq(events.djId, djId), gte(events.date, today)));
|
||||
|
||||
const [thisMonth] = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(events)
|
||||
.where(
|
||||
and(
|
||||
eq(events.djId, djId),
|
||||
gte(events.date, monthStart),
|
||||
lte(events.date, monthEnd)
|
||||
)
|
||||
);
|
||||
|
||||
const [pendingRequests] = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(removalRequests)
|
||||
.where(and(eq(removalRequests.djId, djId), eq(removalRequests.status, "pending")));
|
||||
|
||||
const [totalEvents] = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(events)
|
||||
.where(eq(events.djId, djId));
|
||||
|
||||
return {
|
||||
upcomingEvents: upcomingEvents.count,
|
||||
thisMonth: thisMonth.count,
|
||||
pendingRequests: pendingRequests.count,
|
||||
totalEvents: totalEvents.count,
|
||||
};
|
||||
}
|
||||
|
||||
async getAdminStats(): Promise<{
|
||||
totalDJs: number;
|
||||
activeEvents: number;
|
||||
pendingRequests: number;
|
||||
thisMonth: number;
|
||||
}> {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const currentMonth = new Date().getMonth() + 1;
|
||||
const currentYear = new Date().getFullYear();
|
||||
const monthStart = `${currentYear}-${currentMonth.toString().padStart(2, '0')}-01`;
|
||||
const monthEnd = `${currentYear}-${currentMonth.toString().padStart(2, '0')}-31`;
|
||||
|
||||
const [totalDJs] = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(users)
|
||||
.where(and(eq(users.role, "dj"), eq(users.isActive, true)));
|
||||
|
||||
const [activeEvents] = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(events)
|
||||
.where(gte(events.date, today));
|
||||
|
||||
const [pendingRequests] = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(removalRequests)
|
||||
.where(eq(removalRequests.status, "pending"));
|
||||
|
||||
const [thisMonth] = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(events)
|
||||
.where(and(gte(events.date, monthStart), lte(events.date, monthEnd)));
|
||||
|
||||
return {
|
||||
totalDJs: totalDJs.count,
|
||||
activeEvents: activeEvents.count,
|
||||
pendingRequests: pendingRequests.count,
|
||||
thisMonth: thisMonth.count,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const storage = new DatabaseStorage();
|
||||
@@ -0,0 +1,85 @@
|
||||
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 as const,
|
||||
};
|
||||
|
||||
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"));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user