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:
spliceboti
2025-07-09 23:54:32 +00:00
parent 2869d657c8
commit 89946fcef9
106 changed files with 20207 additions and 0 deletions
+15
View File
@@ -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 });
+70
View File
@@ -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}`);
});
})();
+157
View File
@@ -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;
}
};
+566
View File
@@ -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;
}
+558
View File
@@ -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();
+85
View File
@@ -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"));
});
}