Skip to content

Commit

Permalink
user management
Browse files Browse the repository at this point in the history
  • Loading branch information
magland committed Feb 20, 2025
1 parent ffa5ef7 commit 9ee8ecf
Show file tree
Hide file tree
Showing 18 changed files with 1,193 additions and 32 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changes

## February 20, 2025
- Added user management interface in Settings page for administrators
- Added admin secret key field in Settings for accessing user management
- Added ability to create, edit, and delete users with research descriptions
- Added user list view with basic information display
- Added confirmation dialog for user deletion to prevent accidental removal
- Added API key viewing capability for administrators with copy to clipboard functionality

## February 19, 2025
- Added advanced search mode for DANDI Archive Browser with neurodata type filtering
- Added matching files count indicator for advanced search results in DANDI Archive Browser
Expand Down
6 changes: 3 additions & 3 deletions job-manager/.env.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# MongoDB connection string
MONGODB_URI=mongodb://localhost:27017/neurosift_jobs

# API Keys for authentication
API_SUBMIT_KEY=your-submit-key-here
# Admin configuration
ADMIN_SECRET_KEY=your-secure-admin-secret-key-here

# Memobin API Key for file storage
MEMOBIN_API_KEY=your-memobin-key-here
MEMOBIN_API_KEY=the-memobin-key-here
2 changes: 0 additions & 2 deletions job-manager/app/api/jobs/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,6 @@ export async function GET(
* - Success: Updated job object
* - Error: 404 if job not found, 500 for server errors
*
* Required API key: 'fulfill'
*
* Only specified fields will be updated, maintaining data integrity
*/
export async function PATCH(
Expand Down
12 changes: 7 additions & 5 deletions job-manager/app/api/jobs/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ export async function OPTIONS() {
* 5. Return the job ID (either new or existing)
*/
export async function POST(request: NextRequest) {
// Job creation still requires API key for security
const authError = validateApiKey(request);
if (authError) return authError;
// Validate API key and get user info
const auth = await validateApiKey(request);
if (auth instanceof NextResponse) return auth;

try {
const body = await request.json();
Expand All @@ -64,10 +64,11 @@ export async function POST(request: NextRequest) {
// Convert input to string if it's an object
const inputString = typeof input === 'object' ? JSON.stringify(input) : input.toString();

// Check for existing job with same type and input
// Check for existing job with same type, input, and user
const existingJob = await Job.findOne({
type,
input: inputString,
userId: auth.userId,
status: { $in: ['pending', 'running', 'completed'] }
});

Expand All @@ -79,7 +80,8 @@ export async function POST(request: NextRequest) {
type,
input: inputString,
status: 'pending',
progress: 0
progress: 0,
userId: auth.userId
};

const job = await Job.create(newJob);
Expand Down
9 changes: 4 additions & 5 deletions job-manager/app/api/jobs/search/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,15 @@ export async function OPTIONS() {
* - Success: Array of matching jobs (limited to 100, sorted by creation date)
* - Error: 500 for server errors
*
* Accepted API keys: 'submit' or 'fulfill'
*
* The endpoint will:
* 1. Build a query based on provided filters
* 2. Return the most recent 100 matching jobs
*/
export async function POST(request: NextRequest) {
// Accept either submit or fulfill API keys
const submitKeyError = validateApiKey(request);
if (submitKeyError) return submitKeyError;
const authResult = await validateApiKey(request);
if (authResult instanceof NextResponse) {
return authResult;
}

try {
const body = await request.json();
Expand Down
43 changes: 43 additions & 0 deletions job-manager/app/api/users/[userId]/key/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { NextRequest, NextResponse } from 'next/server';
import connectDB, { User } from '../../../../../lib/db';
import { validateApiKey } from '../../../../../middleware/auth';

export async function OPTIONS() {
return new NextResponse(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': 'http://localhost:5173',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
}
});
}

export async function GET(
request: NextRequest,
{ params }: { params: { userId: string } }
) {
// Only admin can retrieve API keys
const adminKey = process.env.ADMIN_SECRET_KEY;
const authHeader = request.headers.get('Authorization');
const isAdmin = adminKey && authHeader?.split(' ')[1] === adminKey;

if (!isAdmin) {
return new NextResponse('Unauthorized - Admin access required', { status: 401 });
}

try {
await connectDB();

const user = await User.findOne({ userId: params.userId });

if (!user) {
return new NextResponse('User not found', { status: 404 });
}

return NextResponse.json({ apiKey: user.apiKey });
} catch (error) {
console.error('Error retrieving API key:', error);
return new NextResponse('Internal Server Error', { status: 500 });
}
}
150 changes: 150 additions & 0 deletions job-manager/app/api/users/[userId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { NextRequest, NextResponse } from 'next/server';
import connectDB, { User } from '../../../../lib/db';
import { validateApiKey } from '../../../../middleware/auth';

/**
* Handle CORS preflight requests
*/
export async function OPTIONS() {
return new NextResponse(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': 'http://localhost:5173',
'Access-Control-Allow-Methods': 'GET, PUT, DELETE, OPTIONS, POST',
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
}
});
}

/**
* Get user details
* Requires either:
* - Admin secret key
* - User's own API key
*/
export async function GET(
request: NextRequest,
{ params }: { params: { userId: string } }
) {
// Admin key allows access to any user
const adminKey = process.env.ADMIN_SECRET_KEY;
const authHeader = request.headers.get('Authorization');
const isAdmin = adminKey && authHeader?.split(' ')[1] === adminKey;

if (!isAdmin) {
const auth = await validateApiKey(request);
if (auth instanceof NextResponse) return auth;
if (auth.userId !== params.userId) {
return new NextResponse('Unauthorized', { status: 401 });
}
}

try {
await connectDB();

const user = await User.findOne(
{ userId: params.userId },
{ apiKey: 0 } // Exclude API key from response
);

if (!user) {
return new NextResponse('User not found', { status: 404 });
}

return NextResponse.json({ user });
} catch (error) {
console.error('Error getting user:', error);
return new NextResponse('Internal Server Error', { status: 500 });
}
}

/**
* Update user details
* Requires either:
* - Admin secret key
* - User's own API key
*/
export async function DELETE(
request: NextRequest,
{ params }: { params: { userId: string } }
) {
// Only admin can delete users
const adminKey = process.env.ADMIN_SECRET_KEY;
const authHeader = request.headers.get('Authorization');
const isAdmin = adminKey && authHeader?.split(' ')[1] === adminKey;

if (!isAdmin) {
return new NextResponse('Unauthorized - Admin access required', { status: 401 });
}

try {
await connectDB();

const result = await User.deleteOne({ userId: params.userId });

if (result.deletedCount === 0) {
return new NextResponse('User not found', { status: 404 });
}

return new NextResponse('User deleted successfully', { status: 200 });
} catch (error) {
console.error('Error deleting user:', error);
return new NextResponse('Internal Server Error', { status: 500 });
}
}

/**
* Update user details
* Requires either:
* - Admin secret key
* - User's own API key
*/
export async function PUT(
request: NextRequest,
{ params }: { params: { userId: string } }
) {
// Admin key allows access to any user
const adminKey = process.env.ADMIN_SECRET_KEY;
const authHeader = request.headers.get('Authorization');
const isAdmin = adminKey && authHeader?.split(' ')[1] === adminKey;

// Users can only update their own details unless they're admin
if (!isAdmin && auth.userId !== params.userId) {
return new NextResponse('Unauthorized', { status: 401 });
}

try {
const body = await request.json();
const { name, email, researchDescription } = body;

await connectDB();

const user = await User.findOne({ userId: params.userId });

if (!user) {
return new NextResponse('User not found', { status: 404 });
}

// Update allowed fields
if (name) user.name = name;
if (email) user.email = email;
if (researchDescription) user.researchDescription = researchDescription;

await user.save();

// Return updated user without API key
const userResponse = {
userId: user.userId,
name: user.name,
email: user.email,
researchDescription: user.researchDescription,
createdAt: user.createdAt,
updatedAt: user.updatedAt
};

return NextResponse.json({ user: userResponse });
} catch (error) {
console.error('Error updating user:', error);
return new NextResponse('Internal Server Error', { status: 500 });
}
}
102 changes: 102 additions & 0 deletions job-manager/app/api/users/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';
import connectDB, { User } from '../../../lib/db';

/**
* Handle CORS preflight requests
*/
export async function OPTIONS() {
return new NextResponse(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': 'http://localhost:5173',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
}
});
}

/**
* Create a new user with a unique API key
* Requires admin secret key for authorization
*/
export async function POST(request: NextRequest) {
// Only admin can create users
const adminKey = process.env.ADMIN_SECRET_KEY;
if (!adminKey) {
return new NextResponse('Admin secret key not configured', { status: 500 });
}

const authHeader = request.headers.get('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== adminKey) {
return new NextResponse('Unauthorized', { status: 401 });
}

try {
const body = await request.json();
const { name, email, researchDescription } = body;

if (!name || !email || !researchDescription) {
return new NextResponse('Missing required fields', { status: 400 });
}

await connectDB();

// Generate unique user ID and API key
const userId = crypto.randomBytes(16).toString('hex');
const apiKey = crypto.randomBytes(32).toString('hex');

const newUser = await User.create({
userId,
apiKey,
name,
email,
researchDescription
});

// Only return the API key during user creation
return NextResponse.json({
user: {
userId: newUser.userId,
name: newUser.name,
email: newUser.email,
researchDescription: newUser.researchDescription,
createdAt: newUser.createdAt,
apiKey // Include API key only in creation response
}
});
} catch (error) {
console.error('Error creating user:', error);
return new NextResponse('Internal Server Error', { status: 500 });
}
}

/**
* List all users (without API keys)
* Requires admin secret key for authorization
*/
export async function GET(request: NextRequest) {
// Only admin can list users
const adminKey = process.env.ADMIN_SECRET_KEY;
if (!adminKey) {
return new NextResponse('Admin secret key not configured', { status: 500 });
}

const authHeader = request.headers.get('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== adminKey) {
return new NextResponse('Unauthorized', { status: 401 });
}

try {
await connectDB();

const users = await User.find({}, {
apiKey: 0 // Exclude API keys from the response
}).sort({ createdAt: -1 });

return NextResponse.json({ users });
} catch (error) {
console.error('Error listing users:', error);
return new NextResponse('Internal Server Error', { status: 500 });
}
}
Loading

0 comments on commit 9ee8ecf

Please sign in to comment.