Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions firestore.rules
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@ rules_version = '2';

service cloud.firestore {
match /databases/{database}/documents {
// Hosts collection - admin and host roles can manage
match /hosts/{hostId} {
allow read: if request.auth != null;
allow write: if request.auth != null &&
(('admin' in request.auth.token.roles) || ('host' in request.auth.token.roles));
}

// Club settings - admin and host roles can manage
match /club/{clubId} {
allow read: if request.auth != null;
allow write: if request.auth != null &&
(('admin' in request.auth.token.roles) || ('host' in request.auth.token.roles));
}

// Default rule for all other documents
match /{document=**} {
allow read: if request.auth != null;
allow write: if request.auth != null &&
Expand Down
3,545 changes: 1,726 additions & 1,819 deletions functions/package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"firebase": "^11.6.0",
"firebase-admin": "^13.6.0",
"firebase-functions": "^7.0.1",
"firebase-tools": "^14.4.0",
"firebase-tools": "^15.1.0",
"google-auth-library": "^9.14.1",
"google-spreadsheet": "^4.1.4"
},
Expand Down
23 changes: 22 additions & 1 deletion storage.rules
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,28 @@ service firebase.storage {

// Only admins/hosts can upload or modify lineup posters
allow write: if request.auth != null &&
(("admin" in request.auth.token.roles) || ("host" in request.auth.token.roles));
request.auth.token.roles.hasAny(['admin', 'host']);

Copilot AI Jan 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The storage rules use different syntax patterns inconsistently. Line 21 uses the newer hasAny() method with an array literal, while lines 9 and 16 in firestore.rules use the older 'in' operator. Consider standardizing on the hasAny() method for consistency and better readability across both rule files.

Copilot uses AI. Check for mistakes.
}

// Allow hosts and admins to manage host poster images under
// host-posters/{hostId}/{filename}
match /host-posters/{hostId}/{fileName} {
// Anyone can read host posters
allow read: if true;

// Only admins/hosts can upload or modify host posters
allow write: if request.auth != null &&
request.auth.token.roles.hasAny(['admin', 'host']);
}

// Allow hosts and admins to manage the default poster image
match /default-poster/{fileName} {
// Anyone can read the default poster
allow read: if true;

// Only admins/hosts can upload or modify the default poster
allow write: if request.auth != null &&
request.auth.token.roles.hasAny(['admin', 'host']);
}
}
}
789 changes: 478 additions & 311 deletions webapp/package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions webapp/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { EventSignupStart } from './features/eventSignup/EventSignupStart';
import { EventSignupRoot } from './features/eventSignup/EventSignupRoot';
import BingoHost from './features/bingo/BingoHost';
import BingoPlayer from './features/bingo/BingoPlayer';
import ClubSettings from './features/clubSettings/ClubSettings';


function App() {
Expand Down Expand Up @@ -94,6 +95,10 @@ function App() {
path: "userInfo",
element: <UserInfo />
},
{
path: "clubSettings",
element: <RoleGuard requireAnyRole={['host', 'admin']}><ClubSettings /></RoleGuard>
},
],
},
{
Expand Down
6 changes: 4 additions & 2 deletions webapp/src/contexts/useEventDjCache/eventDjDataContext.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { createContext } from "react"
import { Dj, Event } from '../../util/types';
import { DjCache, EventCache } from "./types";
import { Dj, Event, Host } from '../../util/types';
import { DjCache, EventCache, HostCache } from "./types";

type EventDjDataContextType = {
eventCache: EventCache;
djCache: DjCache;
hostCache: HostCache;
loading: boolean;
reloadDj: (id: string) => Promise<Dj | null>;
reloadHost: (id: string) => Promise<Host | null>;
getEventWithDjs: (id: string) => {
event: Event;
djs: (Dj | "PENDING")[];
Expand Down
3 changes: 2 additions & 1 deletion webapp/src/contexts/useEventDjCache/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Dj, Event } from "../../util/types";
import { Dj, Event, Host } from '../../util/types';

export type DjCache = Map<string, Dj>;
export type HostCache = Map<string, Host>;
export type EventCache = Map<string, Event>;
41 changes: 36 additions & 5 deletions webapp/src/contexts/useEventDjCache/useEventDjData.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { useState, useEffect, useCallback } from 'react';
import { Dj, Event } from '../../util/types';
import { Dj, Event, Host } from '../../util/types';
import { collection, doc, getDoc, getDocs, query } from 'firebase/firestore';
import { db } from '../../util/firebase';
import { docToEvent } from '../../store/converters';
import { DjCache, EventCache } from './types';
import { docToEvent, docToHost } from '../../store/converters';
import { DjCache, EventCache, HostCache } from './types';
import { getDjCache } from './util';

export type EventDjStatus = 'idle' | 'loading' | 'ready' | 'error';

export function useEventDjData() {
const [eventCache, setEventCache] = useState<EventCache>(new Map());
const [djCache, setDjCache] = useState<DjCache>(new Map());
const [hostCache, setHostCache] = useState<HostCache>(new Map());
const [status, setStatus] = useState<EventDjStatus>('idle');
const [error, setError] = useState<unknown>(null);

Expand All @@ -19,6 +20,18 @@ export function useEventDjData() {
setDjCache(cache);
}, []);

const reloadAllHosts = useCallback(async () => {
const q = query(collection(db, 'hosts'));
const querySnapshot = await getDocs(q);
const map: HostCache = new Map();

querySnapshot.docs.forEach(docSnap => {
map.set(docSnap.id, docToHost(docSnap));
});

setHostCache(map);
}, []);

const reloadDj = useCallback(async (id: string): Promise<Dj | null> => {
const docRef = doc(db, 'djs', id);
const docSnapshot = await getDoc(docRef);
Expand All @@ -34,6 +47,21 @@ export function useEventDjData() {
return dj;
}, []);

const reloadHost = useCallback(async (id: string): Promise<Host | null> => {
const docRef = doc(db, 'hosts', id);
const docSnapshot = await getDoc(docRef);

if (!docSnapshot.exists()) return null;

const host = docToHost(docSnapshot);
setHostCache(prev => {
const next = new Map(prev);
next.set(id, host);
return next;
});
return host;
}, []);

const reloadAllEvents = useCallback(async () => {
const q = query(collection(db, 'events'));
const querySnapshot = await getDocs(q);
Expand All @@ -53,7 +81,7 @@ export function useEventDjData() {
setStatus('loading');
setError(null);
try {
await Promise.all([reloadAllDjs(), reloadAllEvents()]);
await Promise.all([reloadAllDjs(), reloadAllHosts(), reloadAllEvents()]);
if (!cancelled) setStatus('ready');
} catch (e) {
if (!cancelled) {
Expand All @@ -68,7 +96,7 @@ export function useEventDjData() {
return () => {
cancelled = true;
};
}, [reloadAllDjs, reloadAllEvents]);
}, [reloadAllDjs, reloadAllHosts, reloadAllEvents]);

const getEventWithDjs = useCallback(
(id: string) => {
Expand Down Expand Up @@ -126,13 +154,16 @@ export function useEventDjData() {
return {
eventCache,
djCache,
hostCache,
status,
loading,
ready,
error,
reloadAllDjs,
reloadAllHosts,
reloadAllEvents,
reloadDj,
reloadHost,
getEventWithDjs,
getEventsByDjId,
getPlayedDjsForEvent,
Expand Down
29 changes: 29 additions & 0 deletions webapp/src/features/clubSettings/ClubSettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useState } from "react";
import { Container, Tabs, Tab } from "react-bootstrap";
import HostList from "./HostList";
import DefaultPosterSettings from "./DefaultPosterSettings";

const ClubSettings = () => {
const [activeTab, setActiveTab] = useState<string>("hosts");

return (
<Container className="mt-4">
<h1 className="display-5 mb-4">Club Settings</h1>

<Tabs
activeKey={activeTab}
onSelect={(k) => setActiveTab(k || "hosts")}
className="mb-3"
>
<Tab eventKey="hosts" title="Hosts">
<HostList />
</Tab>
<Tab eventKey="defaultPoster" title="Default Poster">
<DefaultPosterSettings />
</Tab>
</Tabs>
</Container>
);
};

export default ClubSettings;
Loading
Loading