Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 3 additions & 5 deletions src/app/api/admin/pending-count/route.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { NextResponse } from 'next/server';
import { getSessionFromCookies, getUserById, pendingCount, recentPendingUsers } from '@/lib/auth';
import { pendingCount, recentPendingUsers, requireAdmin } from '@/lib/auth';

export const dynamic = 'force-dynamic';

export async function GET() {
const sess = await getSessionFromCookies();
if (!sess) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const me = getUserById(sess.uid);
if (!me || !me.is_admin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
const gate = await requireAdmin();
if (gate instanceof NextResponse) return gate;
const count = pendingCount();
const latest = recentPendingUsers(20).map((u) => ({
id: u.id,
Expand Down
9 changes: 4 additions & 5 deletions src/app/api/admin/users/[id]/approve/route.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { NextResponse } from 'next/server';
import { approveUser, getSessionFromCookies, getUserById } from '@/lib/auth';
import { approveUser, requireAdmin } from '@/lib/auth';

export const dynamic = 'force-dynamic';

export async function POST(_req: Request, ctx: { params: Promise<{ id: string }> }) {
const params = await ctx.params;
const sess = await getSessionFromCookies();
if (!sess) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const me = getUserById(sess.uid);
if (!me || !me.is_admin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
const gate = await requireAdmin();
if (gate instanceof NextResponse) return gate;
const { user: me } = gate;
const id = Number(params.id);
if (!Number.isFinite(id)) return NextResponse.json({ error: 'Invalid id' }, { status: 400 });
const updated = approveUser(id, me.id);
Expand Down
9 changes: 4 additions & 5 deletions src/app/api/admin/users/[id]/demote/route.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { NextResponse } from 'next/server';
import { demoteUser, getSessionFromCookies, getUserById, RoleError } from '@/lib/auth';
import { demoteUser, requireAdmin, RoleError } from '@/lib/auth';

export const dynamic = 'force-dynamic';

export async function POST(_req: Request, ctx: { params: Promise<{ id: string }> }) {
const params = await ctx.params;
const sess = await getSessionFromCookies();
if (!sess) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const me = getUserById(sess.uid);
if (!me || !me.is_admin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
const gate = await requireAdmin();
if (gate instanceof NextResponse) return gate;
const { user: me } = gate;
const id = Number(params.id);
if (!Number.isFinite(id)) return NextResponse.json({ error: 'Invalid id' }, { status: 400 });
try {
Expand Down
9 changes: 4 additions & 5 deletions src/app/api/admin/users/[id]/promote/route.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { NextResponse } from 'next/server';
import { getSessionFromCookies, getUserById, promoteUser, RoleError } from '@/lib/auth';
import { promoteUser, requireAdmin, RoleError } from '@/lib/auth';

export const dynamic = 'force-dynamic';

export async function POST(_req: Request, ctx: { params: Promise<{ id: string }> }) {
const params = await ctx.params;
const sess = await getSessionFromCookies();
if (!sess) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const me = getUserById(sess.uid);
if (!me || !me.is_admin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
const gate = await requireAdmin();
if (gate instanceof NextResponse) return gate;
const { user: me } = gate;
const id = Number(params.id);
if (!Number.isFinite(id)) return NextResponse.json({ error: 'Invalid id' }, { status: 400 });
try {
Expand Down
9 changes: 4 additions & 5 deletions src/app/api/admin/users/[id]/reject/route.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { NextResponse } from 'next/server';
import { getSessionFromCookies, getUserById, rejectUser } from '@/lib/auth';
import { rejectUser, requireAdmin } from '@/lib/auth';

export const dynamic = 'force-dynamic';

export async function POST(_req: Request, ctx: { params: Promise<{ id: string }> }) {
const params = await ctx.params;
const sess = await getSessionFromCookies();
if (!sess) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const me = getUserById(sess.uid);
if (!me || !me.is_admin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
const gate = await requireAdmin();
if (gate instanceof NextResponse) return gate;
const { user: me } = gate;
const id = Number(params.id);
if (!Number.isFinite(id)) return NextResponse.json({ error: 'Invalid id' }, { status: 400 });
const updated = rejectUser(id, me.id);
Expand Down
9 changes: 4 additions & 5 deletions src/app/api/admin/users/route.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { NextResponse } from 'next/server';
import { getSessionFromCookies, getUserById, listUsers } from '@/lib/auth';
import { listUsers, requireAdmin } from '@/lib/auth';

export const dynamic = 'force-dynamic';

export async function GET() {
const sess = await getSessionFromCookies();
if (!sess) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const me = getUserById(sess.uid);
if (!me || !me.is_admin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
const gate = await requireAdmin();
if (gate instanceof NextResponse) return gate;
const { user: me } = gate;
const users = listUsers();
return NextResponse.json({
me: { id: me.id, github_login: me.github_login },
Expand Down
14 changes: 3 additions & 11 deletions src/app/api/user-repos/route.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
import { NextRequest, NextResponse } from 'next/server';
import { getDb } from '@/lib/db';
import { getSessionFromCookies, getUserById } from '@/lib/auth';
import { requireAdmin } from '@/lib/auth';
import type { UserRepo } from '@/types/entities';

export const dynamic = 'force-dynamic';

async function requireAdmin() {
const sess = await getSessionFromCookies();
if (!sess) return { error: NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) };
const me = getUserById(sess.uid);
if (!me || !me.is_admin) return { error: NextResponse.json({ error: 'Forbidden — admin only' }, { status: 403 }) };
return { error: null as null };
}

export async function GET() {
const db = getDb();
const rows = db
Expand All @@ -23,7 +15,7 @@ export async function GET() {

export async function POST(req: NextRequest) {
const gate = await requireAdmin();
if (gate.error) return gate.error;
if (gate instanceof NextResponse) return gate;

const body = await req.json().catch(() => null);
if (!body || typeof body.full_name !== 'string') {
Expand All @@ -47,7 +39,7 @@ export async function POST(req: NextRequest) {

export async function DELETE(req: NextRequest) {
const gate = await requireAdmin();
if (gate.error) return gate.error;
if (gate instanceof NextResponse) return gate;

const url = new URL(req.url);
const fullName = url.searchParams.get('full_name');
Expand Down
17 changes: 17 additions & 0 deletions src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { randomBytes } from 'crypto';
import fs from 'fs';
import path from 'path';
import { cookies, headers } from 'next/headers';
import { NextResponse } from 'next/server';
import { getDb } from '@/lib/db';
import {
encodeSession,
verifySessionToken,
SESSION_COOKIE_NAME,
SESSION_MAX_AGE_SEC,
type SessionPayload,
type SessionStatus,
} from '@/lib/session-token';

Expand Down Expand Up @@ -96,6 +98,21 @@ export async function getSessionFromCookies() {
return verifySessionToken(token);
}

/**
* Admin auth gate for API routes. On success returns the verified session and
* the corresponding fresh user row. On failure returns a 401/403 NextResponse
* the caller can return as-is.
*/
export async function requireAdmin(): Promise<
{ session: SessionPayload; user: UserRow } | NextResponse
> {
const session = await getSessionFromCookies();
if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const user = getUserById(session.uid);
if (!user || !user.is_admin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
return { session, user };
}

export { SESSION_COOKIE_NAME };

// ---------------------------------------------------------------------------
Expand Down