Skip to content

Commit fccdeff

Browse files
authored
Merge pull request #72 from OpencodeNIT-R/development
Development
2 parents a340b11 + 239a565 commit fccdeff

16 files changed

Lines changed: 176 additions & 77 deletions

File tree

‎README.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -240,10 +240,12 @@ Staff open the dashboard from their profile on the site. Access depends on `prof
240240
| ------------------- | ------------------------------------------------------------------------ |
241241
| `user` | Register for the fest and view their pass. |
242242
| `registration-team` | Review registrations and payment proofs, approve or reject them. |
243+
| `viewer` | See what the registration team sees and export it, but change nothing. |
243244
| `it-team` | Manage events and gallery photos. |
244245
| `admin` | Everything above, plus managing users' roles. |
245246

246-
Admins can switch users between `user`, `it-team` and `registration-team`. The `admin` role itself can only be granted
247+
Admins can switch users between `user`, `it-team`, `registration-team` and `viewer` (run
248+
`supabase/manual/add_viewer_role.sql` once before giving anyone `viewer`). The `admin` role itself can only be granted
247249
directly in the database:
248250

249251
```sql
@@ -263,8 +265,8 @@ update public.profiles set role = 'admin' where email = 'someone@example.com';
263265
| `/api/auth/logout` | POST | Signed-in user |
264266
| `/api/register` | GET, POST | Signed-in user |
265267
| `/api/upload` | POST | Signed-in user (payment proof) |
266-
| `/api/admin/registrations` | GET, PATCH | Admin, registration team |
267-
| `/api/admin/registrations/payment-proof` | GET | Admin, registration team |
268+
| `/api/admin/registrations` | GET, PATCH | Admin, registration team (GET: also viewer) |
269+
| `/api/admin/registrations/payment-proof` | GET | Admin, registration team, viewer |
268270
| `/api/admin/events` | GET, POST, PATCH, DELETE | Admin, IT team |
269271
| `/api/admin/gallery` | GET, POST, PATCH, DELETE | Admin, IT team |
270272
| `/api/admin/users` | GET, PATCH | Admin |

‎src/app/api/admin/ambassadors/route.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { NextRequest, NextResponse } from 'next/server';
2-
import { verifyRegistrationStaff } from '@/lib/auth-server';
2+
import { verifyRegistrationStaff, verifyRegistrationViewer } from '@/lib/auth-server';
33
import { isUuid, readJsonObject } from '@/lib/security';
44

55
export const runtime = 'nodejs';
66

77
/**
8-
* Campus ambassadors, for admins and the registration team only (verifyRegistrationStaff): the AMBASSADORS tab of the
9-
* admin dashboard is the programme's only portal. Students become ambassadors themselves (GET MY CODE, POST
8+
* Campus ambassadors, for admins and the registration team only (verifyRegistrationStaff; the read-only 'viewer' role
9+
* may GET, verifyRegistrationViewer): the AMBASSADORS tab of the admin dashboard is the programme's only portal. Students become ambassadors themselves (GET MY CODE, POST
1010
* /api/ambassador/code); staff watch the counts, contact them and revoke a code that is abused. Everything runs as the
1111
* service role: no client role can read or write the table or the leaderboard view (supabase/schema.sql, section 16).
1212
*
@@ -32,7 +32,7 @@ const dbError = (where: string, error: unknown) => {
3232

3333
export async function GET(req: NextRequest) {
3434
try {
35-
const auth = await verifyRegistrationStaff(req);
35+
const auth = await verifyRegistrationViewer(req);
3636
if ('error' in auth) {
3737
return NextResponse.json({ error: auth.error }, { status: auth.status });
3838
}

‎src/app/api/admin/referrals/route.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { NextRequest, NextResponse } from 'next/server';
2-
import { verifyRegistrationStaff } from '@/lib/auth-server';
2+
import { verifyRegistrationViewer } from '@/lib/auth-server';
33
import { sanitizeFilterValue } from '@/lib/security';
44

55
export const runtime = 'nodejs';
@@ -8,13 +8,13 @@ const REFERRALS_PAGE_SIZE = 30;
88

99
/**
1010
* GET /api/admin/referrals?page=1&q=
11-
* Referral leaderboard for admins and the registration team: one row per registrant whose code was used by at least
11+
* Referral leaderboard for admins, the registration team and viewers: one row per registrant whose code was used by at least
1212
* one confirmed or pending registration (public.referral_leaderboard, supabase/schema.sql section 14), most confirmed
1313
* referrals first. Paged on the server, REFERRALS_PAGE_SIZE rows per page; `q` searches name, email, college and code.
1414
*/
1515
export async function GET(req: NextRequest) {
1616
try {
17-
const auth = await verifyRegistrationStaff(req);
17+
const auth = await verifyRegistrationViewer(req);
1818
if ('error' in auth) {
1919
return NextResponse.json({ error: auth.error }, { status: auth.status });
2020
}

‎src/app/api/admin/registrations/id-card/route.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { NextRequest, NextResponse } from 'next/server';
2-
import { verifyRegistrationStaff } from '@/lib/auth-server';
2+
import { verifyRegistrationStaff, verifyRegistrationViewer } from '@/lib/auth-server';
33
import { isIdCardPath, isUuid, readJsonObject } from '@/lib/security';
44
import { ID_CARD_URL_TTL_SECONDS, signIdCardUrl } from '@/lib/id-card-storage';
55
import { isIdCardReuploadReason } from '@/lib/id-cards';
@@ -8,13 +8,14 @@ export const runtime = 'nodejs';
88

99
/**
1010
* GET /api/admin/registrations/id-card?id=<registration uuid>
11-
* Registration-Team/Admin only. Returns a short-lived signed URL for the registration's private college ID card photo.
11+
* Registration-Team/Admin/Viewer only. Returns a short-lived signed URL for the registration's private college ID card
12+
* photo.
1213
* Staff have no Storage read access of their own: this route is the only way to view a card (the admin list and the
1314
* gate scanner both use it).
1415
*/
1516
export async function GET(req: NextRequest) {
1617
try {
17-
const auth = await verifyRegistrationStaff(req);
18+
const auth = await verifyRegistrationViewer(req);
1819
if ('error' in auth) {
1920
return NextResponse.json({ error: auth.error }, { status: auth.status });
2021
}

‎src/app/api/admin/registrations/payment-proof/route.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import { NextRequest, NextResponse } from 'next/server';
2-
import { verifyRegistrationStaff } from '@/lib/auth-server';
2+
import { verifyRegistrationViewer } from '@/lib/auth-server';
33
import { isPaymentProofPath, isUuid, PAYMENT_PROOF_URL_TTL_SECONDS } from '@/lib/security';
44
import { signPaymentProofUrl } from '@/lib/payment-proofs';
55

66
export const runtime = 'nodejs';
77

88
/**
99
* GET /api/admin/registrations/payment-proof?id=<registration uuid>
10-
* Registration-Team/Admin only. Returns a short-lived signed URL for the registration's private payment screenshot.
10+
* Registration-Team/Admin/Viewer only. Returns a short-lived signed URL for the registration's private payment
11+
* screenshot.
1112
* Staff have no Storage read access of their own: this route is the only way to view a proof.
1213
*/
1314
export async function GET(req: NextRequest) {
1415
try {
15-
const auth = await verifyRegistrationStaff(req);
16+
const auth = await verifyRegistrationViewer(req);
1617
if ('error' in auth) {
1718
return NextResponse.json({ error: auth.error }, { status: auth.status });
1819
}

‎src/app/api/admin/registrations/route.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { NextRequest, NextResponse } from 'next/server';
22
import type { SupabaseClient } from '@supabase/supabase-js';
3-
import { verifyRegistrationStaff } from '@/lib/auth-server';
3+
import { verifyRegistrationStaff, verifyRegistrationViewer } from '@/lib/auth-server';
44
import { isUuid, readJsonObject, sanitizeFilterValue } from '@/lib/security';
55
import { isRejectionReason } from '@/lib/rejections';
66
import { ID_CARD_REQUIRED_ERROR, isIdCardRequiredError } from '@/lib/id-cards';
@@ -111,7 +111,7 @@ async function withReviewer(supabase: SupabaseClient, registrations: Record<stri
111111
// GET all registrations (with optional filtering)
112112
export async function GET(req: NextRequest) {
113113
try {
114-
const auth = await verifyRegistrationStaff(req);
114+
const auth = await verifyRegistrationViewer(req);
115115
if ('error' in auth) {
116116
return NextResponse.json({ error: auth.error }, { status: auth.status });
117117
}

‎src/app/api/admin/seats/route.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { NextRequest, NextResponse } from 'next/server';
22
import type { SupabaseClient } from '@supabase/supabase-js';
3-
import { verifyAdmin, verifyRegistrationStaff } from '@/lib/auth-server';
3+
import { verifyAdmin, verifyRegistrationViewer } from '@/lib/auth-server';
44
import { readJsonObject } from '@/lib/security';
55
import { seatCountError } from '@/lib/seats';
66

@@ -35,11 +35,11 @@ const UNAVAILABLE =
3535

3636
/**
3737
* GET /api/admin/seats
38-
* The seat counter for the admin dashboard (admins and the registration team).
38+
* The seat counter for the admin dashboard (admins, the registration team and viewers).
3939
*/
4040
export async function GET(req: NextRequest) {
4141
try {
42-
const auth = await verifyRegistrationStaff(req);
42+
const auth = await verifyRegistrationViewer(req);
4343
if ('error' in auth) {
4444
return NextResponse.json({ error: auth.error }, { status: auth.status });
4545
}

‎src/app/api/admin/users/route.ts‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,11 @@ export async function PATCH(req: NextRequest) {
6767
);
6868
}
6969

70-
// Allowed roles to be assigned via the web dashboard: only 'user', 'it-team', or 'registration-team'
71-
const assignableRoles = ['user', 'it-team', 'registration-team'];
70+
// Allowed roles to be assigned via the web dashboard: only 'user', 'it-team', 'registration-team' or 'viewer'
71+
const assignableRoles = ['user', 'it-team', 'registration-team', 'viewer'];
7272
if (typeof role !== 'string' || !assignableRoles.includes(role)) {
7373
return NextResponse.json(
74-
{ error: 'Invalid role. Only "user", "it-team", and "registration-team" can be assigned via the dashboard. Admin roles must be set directly in the database.' },
74+
{ error: 'Invalid role. Only "user", "it-team", "registration-team" and "viewer" can be assigned via the dashboard. Admin roles must be set directly in the database.' },
7575
{ status: 400 }
7676
);
7777
}
@@ -97,14 +97,21 @@ export async function PATCH(req: NextRequest) {
9797
);
9898
}
9999

100-
// Target user profile verified as non-admin -> execute role change to 'user', 'it-team', or 'registration-team'
100+
// Target user profile verified as non-admin -> execute role change to 'user', 'it-team', 'registration-team' or 'viewer'
101101
const { data: updated, error } = await supabase
102102
.from('profiles')
103103
.update({ role, updated_at: new Date().toISOString() })
104104
.eq('id', userId)
105105
.select()
106106
.maybeSingle();
107107

108+
// 23514: the role CHECK of a database without supabase/manual/add_viewer_role.sql refuses 'viewer'.
109+
if (error?.code === '23514' && role === 'viewer') {
110+
return NextResponse.json(
111+
{ error: 'The viewer role is not set up yet. Run supabase/manual/add_viewer_role.sql in Supabase.' },
112+
{ status: 500 }
113+
);
114+
}
108115
if (error) {
109116
console.error('/api/admin/users database error:', error);
110117
return NextResponse.json({ error: 'Database operation failed.' }, { status: 500 });

0 commit comments

Comments
 (0)