-
Notifications
You must be signed in to change notification settings - Fork 11
[Draft] Implement onboarding wizard and allow google SSO login configuration #183
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
13d23e4
c508d4b
3cf03c7
9441c90
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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, | ||
| }; | ||
| } | ||
|
|
||
| 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 }, | ||
| ); | ||
| } | ||
| } | ||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
|
|
||
| // 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()); | ||
| } | ||
| } | ||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic here assumes there will only ever be one onboarding progress record with |
||
|
|
||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using |
||
| 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 }); | ||
| } | ||
| } | ||
| 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 }); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exposing the
clientSecretto 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 theclientIdto initiate the OAuth flow. All operations requiring theclientSecret, such as exchanging the authorization code for a token, must be handled exclusively by the backend.