Skip to content

Commit 583896e

Browse files
Merge pull request #59 from Genesis-ISE-DSCE/fixes
Implement admin token authentication for food distribution API and UI
2 parents ab353f0 + 7d4ffcb commit 583896e

4 files changed

Lines changed: 105 additions & 28 deletions

File tree

frontend/src/app/admin/food/[teamCode]/page.tsx

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,20 @@ export default function FoodDistributionPage() {
4545
const teamCode = params.teamCode as string;
4646

4747
// --- State Hooks ---
48+
const [token, setToken] = useState<string>("");
49+
const [inputToken, setInputToken] = useState<string>("");
4850
const [membersData, setMembersData] = useState<MemberList>([]);
4951
const [teamName, setTeamName] = useState('');
5052
const [loading, setLoading] = useState(true);
5153
const [error, setError] = useState<string | null>(null);
5254

5355
// --- Data Fetching Effect ---
56+
useEffect(() => {
57+
// load token from localStorage
58+
const stored = typeof window !== 'undefined' ? localStorage.getItem('admin_token') : null;
59+
if (stored) setToken(stored);
60+
}, []);
61+
5462
useEffect(() => {
5563
if (!teamCode || teamCode === "undefined") {
5664
setLoading(false);
@@ -62,7 +70,9 @@ export default function FoodDistributionPage() {
6270
setLoading(true);
6371
setError(null);
6472
try {
65-
const response = await fetch(`/api/admin/food/${teamCode}`);
73+
const response = await fetch(`/api/admin/food/${teamCode}`, {
74+
headers: token ? { Authorization: `Bearer ${token}` } : {},
75+
});
6676
if (!response.ok) {
6777
const err = await response.json();
6878
throw new Error(err.message || 'Team not found');
@@ -85,8 +95,10 @@ export default function FoodDistributionPage() {
8595
}
8696
};
8797

88-
fetchTeam();
89-
}, [teamCode]);
98+
if (token) {
99+
fetchTeam();
100+
}
101+
}, [teamCode, token]);
90102

91103
// --- Update Handler ---
92104
const handleFoodToggle = async (
@@ -107,7 +119,7 @@ export default function FoodDistributionPage() {
107119
try {
108120
const response = await fetch('/api/admin/food/update', {
109121
method: 'POST',
110-
headers: { 'Content-Type': 'application/json' },
122+
headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
111123
body: JSON.stringify({
112124
docId: docId,
113125
field: field,
@@ -136,6 +148,60 @@ export default function FoodDistributionPage() {
136148
if (loading) { /* ... */ }
137149
if (error) { /* ... */ }
138150

151+
if (!token) {
152+
return (
153+
<div style={{ maxWidth: 420, margin: '64px auto', padding: 24 }}>
154+
<Toaster />
155+
<h2 style={{ fontSize: 24, fontWeight: 700, marginBottom: 12 }}>Admin Login</h2>
156+
<p style={{ color: '#666', marginBottom: 16 }}>Enter the admin access token.</p>
157+
<form
158+
onSubmit={(e) => {
159+
e.preventDefault();
160+
const trimmed = inputToken.trim();
161+
if (!trimmed) return;
162+
localStorage.setItem('admin_token', trimmed);
163+
setToken(trimmed);
164+
}}
165+
style={{ display: 'flex', gap: 8 }}
166+
>
167+
<input
168+
type="password"
169+
placeholder="Admin token"
170+
value={inputToken}
171+
onChange={(e) => setInputToken(e.target.value)}
172+
style={{ flex: 1, padding: '10px 12px', border: '1px solid #ddd', borderRadius: 8 }}
173+
/>
174+
<button
175+
type="submit"
176+
style={{
177+
padding: '12px 32px',
178+
borderRadius: 12,
179+
background: 'linear-gradient(135deg, #FF0500 0%, #c53030 100%)',
180+
color: 'white',
181+
border: 'none',
182+
fontWeight: 'bold',
183+
textTransform: 'uppercase',
184+
letterSpacing: '1px',
185+
cursor: 'pointer',
186+
transition: 'all 0.3s ease',
187+
boxShadow: '0 4px 15px rgba(255, 5, 0, 0.4)'
188+
}}
189+
onMouseOver={(e) => {
190+
e.currentTarget.style.transform = 'translateY(-2px)';
191+
e.currentTarget.style.boxShadow = '0 6px 20px rgba(255, 5, 0, 0.6)';
192+
}}
193+
onMouseOut={(e) => {
194+
e.currentTarget.style.transform = 'translateY(0)';
195+
e.currentTarget.style.boxShadow = '0 4px 15px rgba(255, 5, 0, 0.4)';
196+
}}
197+
>
198+
Continue
199+
</button>
200+
</form>
201+
</div>
202+
);
203+
}
204+
139205
return (
140206
<div className={styles.pageContainer}>
141207
<Toaster />

frontend/src/app/api/admin/food/[teamCode]/route.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
import { NextResponse } from 'next/server';
22
import dbConnect from '@/lib/dbConnect';
33
import FoodLog from '@/models/FoodLog';
4-
import mongoose from 'mongoose'; // <-- 1. Import mongoose
4+
// mongoose not needed here since FoodLog uses string _id
5+
6+
function isAuthorized(request: Request): boolean {
7+
const header = request.headers.get('authorization') || '';
8+
const token = header.startsWith('Bearer ') ? header.slice(7) : '';
9+
const expected = process.env.ADMIN_TOKEN || '';
10+
return Boolean(expected) && token === expected;
11+
}
512

613
// 2. Define an interface for the data returned by .lean()
714
interface IFoodLog {
8-
_id: mongoose.Types.ObjectId;
15+
_id: string;
916
name: string;
1017
teamName: string;
1118
teamId: string;
@@ -21,6 +28,9 @@ export async function GET(
2128
context: { params: Promise<{ teamCode: string }> }
2229
) {
2330
try {
31+
if (!isAuthorized(request)) {
32+
return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
33+
}
2434
const params = await context.params;
2535
const { teamCode } = params;
2636

@@ -43,13 +53,8 @@ export async function GET(
4353
);
4454
}
4555

46-
// 3. Apply the type to the 'member' variable
47-
const membersData = members.map((member: IFoodLog) => ({
48-
...member,
49-
_id: member._id.toString(), // <-- This line will no longer error
50-
}));
51-
52-
return NextResponse.json(membersData, { status: 200 });
56+
// Data already has string _id; return as-is
57+
return NextResponse.json(members, { status: 200 });
5358
} catch (error: unknown) { // <-- Also fixed the 'any' type error here
5459
let message = 'Internal server error';
5560
if (error instanceof Error) {

frontend/src/app/api/admin/food/update/route.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
import { NextResponse } from 'next/server';
22
import dbConnect from '@/lib/dbConnect';
33
import FoodLog from '@/models/FoodLog';
4-
import mongoose from 'mongoose';
4+
// No ObjectId casting; FoodLog uses string _id
5+
6+
function isAuthorized(request: Request): boolean {
7+
const header = request.headers.get('authorization') || '';
8+
const token = header.startsWith('Bearer ') ? header.slice(7) : '';
9+
const expected = process.env.ADMIN_TOKEN || '';
10+
return Boolean(expected) && token === expected;
11+
}
512

613
export async function POST(request: Request) {
714
try {
15+
if (!isAuthorized(request)) {
16+
return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
17+
}
818
const { docId, field, status } = await request.json();
919

1020
if (!docId || !field || typeof status !== 'boolean') {
@@ -16,19 +26,10 @@ export async function POST(request: Request) {
1626

1727
await dbConnect();
1828

19-
let result;
20-
try {
21-
result = await FoodLog.updateOne(
22-
{ _id: new mongoose.Types.ObjectId(docId) },
23-
{ [field]: status }
24-
);
25-
} catch (castError) {
26-
console.warn('Casting to ObjectId failed, trying string match...');
27-
result = await FoodLog.updateOne(
28-
{ _id: docId },
29-
{ [field]: status }
30-
);
31-
}
29+
const result = await FoodLog.updateOne(
30+
{ _id: docId },
31+
{ [field]: status }
32+
);
3233

3334
if (result.matchedCount === 0) {
3435
return NextResponse.json({ message: 'Document not found' }, { status: 404 });

frontend/src/models/FoodLog.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import mongoose from 'mongoose';
22

33
const FoodLogSchema = new mongoose.Schema({
4+
_id: { type: String },
45
name: { type: String, required: true },
56
teamName: { type: String, required: true },
67
teamId: { type: String, required: true, index: true }, // Add index for fast lookup
@@ -13,4 +14,8 @@ const FoodLogSchema = new mongoose.Schema({
1314
});
1415

1516
// Tell Mongoose to use the 'food1' collection
16-
export default mongoose.models.FoodLog || mongoose.model('FoodLog', FoodLogSchema, 'food1');
17+
// In dev/hot-reload, force recompile to pick up schema changes (string _id)
18+
if (mongoose.models.FoodLog) {
19+
delete mongoose.models.FoodLog;
20+
}
21+
export default mongoose.model('FoodLog', FoodLogSchema, 'food1');

0 commit comments

Comments
 (0)