Skip to content
Open
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,9 +395,10 @@ pnpm run dev

## **πŸ“š Resources**

- [**Contributing Guidelines**](https://github.com/liblaber/ai/blob/main/CONTRIBUTING.md)Β - How to contribute to the project
- [**Contributing Guidelines**](https://github.com/liblaber/ai/blob/main/CONTRIBUTING.md) - How to contribute to the project
- [Security & Privacy](docs/security-and-privacy.md)
- [Configuration](docs/configuration.md)
- [SSO Setup Guide](docs/sso-setup.md) - Configure Single Sign-On with your identity provider
- [Deploy on EC2 with HTTPS & Auto-Restart](docs/ec2.md)
- [Getting Started](docs/getting-started.md)
- [Features](docs/features.md)
Expand Down
46 changes: 46 additions & 0 deletions app/api/auth/google-config-status/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { NextResponse } from 'next/server';
import { GoogleAuthService } from '~/lib/services/googleAuthService';
import { createScopedLogger } from '~/utils/logger';

const logger = createScopedLogger('google-config-status');

export async function GET() {
try {
const config = await GoogleAuthService.getGoogleOAuthConfig();
const source = await GoogleAuthService.getConfigSource();
const isAvailable = await GoogleAuthService.isGoogleOAuthAvailable();

const response: any = {
success: true,
config: {
source,
},
isAvailable,
message:
source === 'environment'
? 'Google OAuth configured via environment variables'
: source === 'onboarding'
? 'Google OAuth configured via onboarding'
: 'Google OAuth not configured',
};

// Include credentials if they're available (for frontend to populate form)
if (isAvailable && config.clientId && config.clientSecret) {
response.credentials = {
clientId: config.clientId,
clientSecret: config.clientSecret,
};
Comment on lines +28 to +32
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

Exposing the clientSecret to the frontend is a critical security vulnerability. The client secret must be kept confidential on the server and should never be sent to the client-side. The frontend only requires the clientId to initiate the OAuth flow. All operations requiring the clientSecret, such as exchanging the authorization code for a token, must be handled exclusively by the backend.

      response.credentials = {
        clientId: config.clientId,
      };

}

return NextResponse.json(response);
} catch (error) {
logger.error('Error getting Google OAuth config status:', error);
return NextResponse.json(
{
success: false,
error: 'Failed to get Google OAuth configuration status',
},
{ status: 500 },
);
}
}
57 changes: 57 additions & 0 deletions app/api/auth/google/callback/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { NextRequest, NextResponse } from 'next/server';
import { createScopedLogger } from '~/utils/logger';

const logger = createScopedLogger('google-oauth-callback');

export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const code = searchParams.get('code');
const error = searchParams.get('error');

// Handle OAuth errors
if (error) {
logger.error('Google OAuth error:', error);

const errorDescription = searchParams.get('error_description') || 'Unknown error';

// Redirect back to onboarding with error
const redirectUrl = new URL('/onboarding', request.url);
redirectUrl.searchParams.set('error', `Google OAuth error: ${errorDescription}`);

return NextResponse.redirect(redirectUrl.toString());
}

// Validate required parameters
if (!code) {
logger.error('No authorization code received from Google');

const redirectUrl = new URL('/onboarding', request.url);
redirectUrl.searchParams.set('error', 'No authorization code received from Google');

return NextResponse.redirect(redirectUrl.toString());
Comment on lines +18 to +32
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The logic for creating a redirect URL to the /onboarding page is duplicated across error handling paths and the success path. This repetition can make the code harder to maintain. Consider extracting this logic into a small helper function to reduce code duplication.

}

// For now, we'll just redirect back to onboarding with success
// In a full implementation, you would:
// 1. Exchange the code for tokens
// 2. Get user info from Google
// 3. Create the admin user
// 4. Set up the session

logger.info('Google OAuth callback successful, redirecting to onboarding');

const redirectUrl = new URL('/onboarding', request.url);
redirectUrl.searchParams.set('google_oauth_success', 'true');
redirectUrl.searchParams.set('code', code);

return NextResponse.redirect(redirectUrl.toString());
} catch (error) {
logger.error('Error in Google OAuth callback:', error);

const redirectUrl = new URL('/onboarding', request.url);
redirectUrl.searchParams.set('error', 'OAuth callback failed');

return NextResponse.redirect(redirectUrl.toString());
}
}
129 changes: 129 additions & 0 deletions app/api/onboarding/auth-config/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '~/lib/prisma';
import { createScopedLogger } from '~/utils/logger';
import {
type OnboardingStepResponse,
type OnboardingSuccessResponse,
type OnboardingErrorResponse,
AUTH_CONFIG_STEP_REQUEST_SCHEMA,
} from '~/types/onboarding';

const logger = createScopedLogger('onboarding-auth-config-api');

export async function POST(request: NextRequest): Promise<NextResponse<OnboardingStepResponse>> {
try {
const body = await request.json();

// Debug logging
logger.info('Received auth-config step request:', JSON.stringify(body, null, 2));

// Validate request body structure using Zod
const validationResult = AUTH_CONFIG_STEP_REQUEST_SCHEMA.safeParse(body);

if (!validationResult.success) {
logger.error('Validation failed:', validationResult.error.errors);

const errorResponse: OnboardingErrorResponse = {
success: false,
error: `Invalid request format: ${validationResult.error.errors.map((e) => e.message).join(', ')}`,
};

return NextResponse.json(errorResponse, { status: 400 });
}

const { adminData, ssoConfig, googleOAuthConfig } = validationResult.data;

// Validate password if using password auth
if (adminData.password && adminData.confirmPassword) {
if (adminData.password !== adminData.confirmPassword) {
const errorResponse: OnboardingErrorResponse = {
success: false,
error: 'Passwords do not match',
};
return NextResponse.json(errorResponse, { status: 400 });
}
}

// Check if onboarding progress already exists
let progress = await prisma.onboardingProgress.findFirst({
where: { userId: null }, // For initial setup, userId is null
});
Comment on lines +48 to +50
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The logic here assumes there will only ever be one onboarding progress record with userId: null. If it's possible for multiple incomplete onboarding sessions to exist (e.g., multiple users attempting to set up the app simultaneously), findFirst without a more specific ordering or filter could lead to updating the wrong record, creating a race condition. Consider adding a unique token for each onboarding session to reliably identify and update the correct progress record.


if (progress) {
// Update existing progress
progress = await prisma.onboardingProgress.update({
where: { id: progress.id },
data: {
adminData: adminData as any,
ssoConfig: ssoConfig as any,
googleOAuthConfig: googleOAuthConfig as any,
Comment on lines +57 to +59
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Using as any casts for adminData, ssoConfig, and googleOAuthConfig bypasses TypeScript's type safety. This can hide potential type mismatches between your application logic and the database schema. A safer approach is to ensure the data conforms to the JsonValue type expected by Prisma, or to use a library like Zod to parse the JSON fields after retrieving them from the database to validate their structure and infer their types.

currentStep: 'complete',
updatedAt: new Date(),
},
});
} else {
// Create new progress
progress = await prisma.onboardingProgress.create({
data: {
adminData: adminData as any,
ssoConfig: ssoConfig as any,
googleOAuthConfig: googleOAuthConfig as any,
currentStep: 'complete',
},
});
}

const successResponse: OnboardingSuccessResponse = {
success: true,
message: 'Authentication configuration saved successfully',
currentStep: 'complete',
};

logger.info('Auth-config step completed successfully');

return NextResponse.json(successResponse);
} catch (error) {
logger.error('Error in auth-config step:', error);

const errorResponse: OnboardingErrorResponse = {
success: false,
error: 'Failed to save authentication configuration',
};

return NextResponse.json(errorResponse, { status: 500 });
}
}

export async function GET(): Promise<NextResponse<OnboardingStepResponse>> {
try {
// Get current auth-config step progress
const progress = await prisma.onboardingProgress.findFirst({
where: { userId: null },
});

if (!progress) {
const errorResponse: OnboardingErrorResponse = {
success: false,
error: 'No onboarding progress found',
};
return NextResponse.json(errorResponse, { status: 404 });
}

const successResponse: OnboardingSuccessResponse = {
success: true,
message: 'Auth-config step progress retrieved',
currentStep: progress.currentStep as any,
};

return NextResponse.json(successResponse);
} catch (error) {
logger.error('Error retrieving auth-config step:', error);

const errorResponse: OnboardingErrorResponse = {
success: false,
error: 'Failed to retrieve auth-config step progress',
};

return NextResponse.json(errorResponse, { status: 500 });
}
}
114 changes: 114 additions & 0 deletions app/api/onboarding/auth/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '~/lib/prisma';
import { createScopedLogger } from '~/utils/logger';
import {
type OnboardingStepResponse,
type OnboardingSuccessResponse,
type OnboardingErrorResponse,
AUTH_STEP_REQUEST_SCHEMA,
} from '~/types/onboarding';

const logger = createScopedLogger('onboarding-auth-api');

export async function POST(request: NextRequest): Promise<NextResponse<OnboardingStepResponse>> {
try {
const body = await request.json();

// Debug logging
logger.info('Received auth step request:', JSON.stringify(body, null, 2));

// Validate request body structure using Zod
const validationResult = AUTH_STEP_REQUEST_SCHEMA.safeParse(body);

if (!validationResult.success) {
logger.error('Validation failed:', validationResult.error.errors);

const errorResponse: OnboardingErrorResponse = {
success: false,
error: `Invalid request format: ${validationResult.error.errors.map((e) => e.message).join(', ')}`,
};

return NextResponse.json(errorResponse, { status: 400 });
}

const { authMethod } = validationResult.data;

// Check if onboarding progress already exists
let progress = await prisma.onboardingProgress.findFirst({
where: { userId: null }, // For initial setup, userId is null
});

if (progress) {
// Update existing progress
progress = await prisma.onboardingProgress.update({
where: { id: progress.id },
data: {
authMethod,
currentStep: 'auth-config',
updatedAt: new Date(),
},
});
} else {
// Create new progress
progress = await prisma.onboardingProgress.create({
data: {
authMethod,
currentStep: 'auth-config',
},
});
}

const successResponse: OnboardingSuccessResponse = {
success: true,
message: 'Authentication method saved successfully',
currentStep: 'auth-config',
};

logger.info('Auth step completed successfully');

return NextResponse.json(successResponse);
} catch (error) {
logger.error('Error in auth step:', error);

const errorResponse: OnboardingErrorResponse = {
success: false,
error: 'Failed to save authentication method',
};

return NextResponse.json(errorResponse, { status: 500 });
}
}

export async function GET(): Promise<NextResponse<OnboardingStepResponse>> {
try {
// Get current auth step progress
const progress = await prisma.onboardingProgress.findFirst({
where: { userId: null },
});

if (!progress) {
const errorResponse: OnboardingErrorResponse = {
success: false,
error: 'No onboarding progress found',
};
return NextResponse.json(errorResponse, { status: 404 });
}

const successResponse: OnboardingSuccessResponse = {
success: true,
message: 'Auth step progress retrieved',
currentStep: progress.currentStep as any,
};

return NextResponse.json(successResponse);
} catch (error) {
logger.error('Error retrieving auth step:', error);

const errorResponse: OnboardingErrorResponse = {
success: false,
error: 'Failed to retrieve auth step progress',
};

return NextResponse.json(errorResponse, { status: 500 });
}
}
Loading