fix: address all code review findings

- Extract shared permission logic into boardPermissions.ts utility
- Fix DnD drag revert: add dirty flag to prevent  overwrite
- Wrap OAuth group sync in Prisma transaction (N+1 fix)
- Add empty widgetIds validation in widget reorder API
- Add invalidateAll() after guest toggle PATCH
- Replace console.error with user-visible error banners
- Extract WidgetCreationForm component (DraggableSection was 448 lines)
- Remove unused boardId prop from DraggableSection
- Always include OAuth state parameter + validate in callback
- Clean up copyLink timer on component destroy
- Add type-specific widget config validation in addWidget action
This commit is contained in:
2026-03-25 00:03:32 +03:00
parent 5a6002be76
commit cba160ecb8
15 changed files with 588 additions and 447 deletions
+8 -3
View File
@@ -14,18 +14,23 @@ export const GET: RequestHandler = async ({ cookies, url }) => {
const appUrl = process.env.APP_URL || url.origin;
const redirectUri = process.env.OAUTH_REDIRECT_URI || `${appUrl}/auth/oauth/callback`;
// Generate PKCE values
// Generate PKCE values and state parameter
const codeVerifier = oauthService.generateCodeVerifier();
const codeChallenge = await oauthService.calculateCodeChallenge(codeVerifier);
const state = oauthService.generateState();
// Store code_verifier in HTTP-only cookie for the callback
// Store code_verifier and state in HTTP-only cookies for the callback
cookies.set('oauth_code_verifier', codeVerifier, {
...COOKIE_BASE,
maxAge: 600 // 10 minutes — enough for the auth flow
});
cookies.set('oauth_state', state, {
...COOKIE_BASE,
maxAge: 600
});
// Build authorization URL and redirect
const authUrl = await oauthService.generateAuthUrl(redirectUri, codeChallenge);
const authUrl = await oauthService.generateAuthUrl(redirectUri, codeChallenge, state);
throw redirect(302, authUrl);
} catch (err) {
+15 -3
View File
@@ -26,17 +26,29 @@ export const GET: RequestHandler = async ({ url, cookies }) => {
throw new Error('No authorization code received from OAuth provider');
}
// Retrieve the code_verifier from the cookie
// Retrieve the code_verifier and state from cookies
const codeVerifier = cookies.get('oauth_code_verifier');
if (!codeVerifier) {
throw new Error('OAuth session expired. Please try logging in again.');
}
// Clear the code_verifier cookie
const expectedState = cookies.get('oauth_state');
if (!expectedState) {
throw new Error('OAuth session expired. Please try logging in again.');
}
// Validate the state parameter matches to prevent CSRF
const returnedState = url.searchParams.get('state');
if (returnedState !== expectedState) {
throw new Error('OAuth state mismatch. Possible CSRF attack. Please try logging in again.');
}
// Clear the OAuth cookies
cookies.delete('oauth_code_verifier', { path: '/' });
cookies.delete('oauth_state', { path: '/' });
// Exchange the authorization code for tokens and get user info
const userInfo = await oauthService.handleCallback(url, codeVerifier);
const userInfo = await oauthService.handleCallback(url, codeVerifier, expectedState);
// Find or create local user from OAuth info
const user = await userService.findOrCreateByOAuth({