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
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ function decodeOAuthState(
}
}

import { useUser } from '@/hooks/useUser';
import { useSetupBootstrapOpen, useUser } from '@/hooks/useUser';
import { useRedirectToSignIn } from '@/hooks/useSignInRedirect';
import {
useFinishAuthenticateGitHubAccount,
Expand All @@ -57,6 +57,7 @@ export default function Page() {
const params = useSearchParams();

const { authStatus, isSignedIn } = useUser();
const setupBootstrapOpen = useSetupBootstrapOpen();

const [isLoading, setIsLoading] = useState(true);
const [isInstalled, setIsInstalled] = useState(false);
Expand All @@ -71,9 +72,11 @@ export default function Page() {
// user back to where those can be fixed instead of dead-ending on the error.
const cameFromSetup =
decodedCallbackState?.redirect?.startsWith('/setup') === true;
const errorReturnTarget = cameFromSetup
? '/setup?step=source-control-config'
: '/settings';
const completedSetupReturnTarget =
cameFromSetup && !setupBootstrapOpen ? '/settings/source-control' : null;
const errorReturnTarget =
completedSetupReturnTarget ??
(cameFromSetup ? '/setup?step=source-control-config' : '/settings');

const navigateFromState = useCallback(() => {
const encodedState = params.get('state');
Expand All @@ -87,8 +90,15 @@ export default function Page() {
!redirect.startsWith('//') &&
!redirect.includes('://');

router.push(isValidRedirect ? redirect : '/settings');
}, [params, router]);
const setupCompletedRedirect =
isValidRedirect && redirect.startsWith('/setup') && !setupBootstrapOpen
? '/settings/source-control'
: null;

router.push(
setupCompletedRedirect ?? (isValidRedirect ? redirect : '/settings'),
);
}, [params, router, setupBootstrapOpen]);

const finishAuthentication = useFinishAuthenticateGitHubAccount({
onSuccess: (result) => {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@ export function InstructionUrl({
url: string;
}) {
return (
<div className="space-y-1 flex gap-2 items-center">
<p className="font-semibold text-foreground text-sm w-50 shrink-0">
<div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:gap-2">
<p className="text-sm font-semibold text-foreground sm:w-50 sm:shrink-0">
{heading}
</p>
<div className="flex items-center gap-2 rounded-md border border-black px-2 py-1.5 overflow-hidden justify-end">
<div className="flex min-w-0 flex-1 items-center justify-between gap-2 rounded-md border border-black px-2 py-1.5">
<BasicTooltip content={url}>
<span className="font-mono text-xs text-foreground truncate">
<span className="min-w-0 break-all font-mono text-xs text-foreground">
{url}
</span>
</BasicTooltip>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NextResponse } from 'next/server';
import { NextRequest, NextResponse } from 'next/server';
import { resolveDeploymentEnvVar } from '@roomote/db/server';
import {
BITBUCKET_OAUTH_CALLBACK_PATH,
Expand All @@ -7,11 +7,16 @@ import {
} from '@roomote/bitbucket';
import { authorize } from '@/lib/server';
import { bootstrapWebRuntimeEnv } from '@/lib/server/bootstrap-runtime-env';
import {
getSourceControlOAuthReturnCookieName,
normalizeSourceControlOAuthReturnTarget,
SOURCE_CONTROL_OAUTH_COOKIE_MAX_AGE,
} from '@/lib/server/source-control-oauth-redirect';

export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';

export async function GET() {
export async function GET(request: NextRequest) {
const authResult = await authorize();
const webEnv = await bootstrapWebRuntimeEnv();
if (!authResult.success || !authResult.isAdmin) {
Expand All @@ -30,12 +35,26 @@ export async function GET() {
redirectUri: buildBitbucketOAuthRedirectUri(publicAppUrl),
});
const response = NextResponse.redirect(url);
const returnTarget = normalizeSourceControlOAuthReturnTarget(
request.nextUrl.searchParams.get('redirectTo'),
);
response.cookies.set('roomote-bitbucket-oauth-state', state, {
httpOnly: true,
sameSite: 'lax',
secure: publicAppUrl.startsWith('https://'),
path: BITBUCKET_OAUTH_CALLBACK_PATH,
maxAge: 600,
maxAge: SOURCE_CONTROL_OAUTH_COOKIE_MAX_AGE,
});
response.cookies.set(
getSourceControlOAuthReturnCookieName('bitbucket'),
returnTarget ?? '',
{
httpOnly: true,
sameSite: 'lax',
secure: publicAppUrl.startsWith('https://'),
path: BITBUCKET_OAUTH_CALLBACK_PATH,
maxAge: SOURCE_CONTROL_OAUTH_COOKIE_MAX_AGE,
},
);
return response;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,47 @@ import {
} from '@roomote/bitbucket';
import { authorize } from '@/lib/server';
import { bootstrapWebRuntimeEnv } from '@/lib/server/bootstrap-runtime-env';
import { getSetupBootstrapState } from '@/lib/server/setup-bootstrap-state';
import { syncRepositoriesCommand } from '@/trpc/commands/source-control';
import {
addSourceControlOAuthResult,
getSourceControlOAuthReturnCookieName,
isSetupOAuthReturnTarget,
resolveSourceControlOAuthReturnTarget,
} from '@/lib/server/source-control-oauth-redirect';

export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';

export async function GET(request: NextRequest) {
const webEnv = await bootstrapWebRuntimeEnv();
const publicAppUrl = webEnv.R_PUBLIC_URL ?? webEnv.R_APP_URL;
const redirect = new URL('/setup', publicAppUrl);
redirect.searchParams.set('step', 'source-control-connect');
const { setupOpen } = await getSetupBootstrapState();
const returnTarget = resolveSourceControlOAuthReturnTarget({
requestedTarget: request.cookies.get(
getSourceControlOAuthReturnCookieName('bitbucket'),
)?.value,
setupOpen,
});
const redirect = new URL(returnTarget, publicAppUrl);
const response = () => NextResponse.redirect(redirect);
const clearCookies = (result: NextResponse) => {
result.cookies.set('roomote-bitbucket-oauth-state', '', {
httpOnly: true,
sameSite: 'lax',
secure: publicAppUrl.startsWith('https://'),
path: BITBUCKET_OAUTH_CALLBACK_PATH,
maxAge: 0,
});
result.cookies.set(getSourceControlOAuthReturnCookieName('bitbucket'), '', {
httpOnly: true,
sameSite: 'lax',
secure: publicAppUrl.startsWith('https://'),
path: BITBUCKET_OAUTH_CALLBACK_PATH,
maxAge: 0,
});
return result;
};
const authResult = await authorize();
const state = request.nextUrl.searchParams.get('state');
const expectedState = request.cookies.get(
Expand All @@ -32,7 +62,7 @@ export async function GET(request: NextRequest) {
!code
) {
redirect.searchParams.set('bitbucket', 'error');
return response();
return clearCookies(response());
}
try {
const [clientId, clientSecret] = await Promise.all([
Expand All @@ -49,20 +79,28 @@ export async function GET(request: NextRequest) {
code,
redirectUri: buildBitbucketOAuthRedirectUri(publicAppUrl),
});
await syncRepositoriesCommand(authResult, { provider: 'bitbucket' });
redirect.searchParams.set('bitbucket', 'connected');
redirect.searchParams.set('sync', '1');
if (!isSetupOAuthReturnTarget(returnTarget)) {
const syncResult = await syncRepositoriesCommand(authResult, {
provider: 'bitbucket',
});
if (!syncResult.success) {
throw new Error(syncResult.error);
}
}
const resultTarget = addSourceControlOAuthResult(
returnTarget,
'bitbucket',
'connected',
);
redirect.href = new URL(resultTarget, publicAppUrl).href;
} catch (error) {
console.error('[Bitbucket OAuth] callback failed', error);
redirect.searchParams.set('bitbucket', 'error');
const resultTarget = addSourceControlOAuthResult(
returnTarget,
'bitbucket',
'error',
);
redirect.href = new URL(resultTarget, publicAppUrl).href;
}
const result = response();
result.cookies.set('roomote-bitbucket-oauth-state', '', {
httpOnly: true,
sameSite: 'lax',
secure: publicAppUrl.startsWith('https://'),
path: BITBUCKET_OAUTH_CALLBACK_PATH,
maxAge: 0,
});
return result;
return clearCookies(response());
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import {
} from '@roomote/gitea';
import { authorize, getCallbackHost } from '@/lib/server';
import { bootstrapWebRuntimeEnv } from '@/lib/server/bootstrap-runtime-env';
import {
getSourceControlOAuthReturnCookieName,
normalizeSourceControlOAuthReturnTarget,
SOURCE_CONTROL_OAUTH_COOKIE_MAX_AGE,
} from '@/lib/server/source-control-oauth-redirect';

export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
Expand Down Expand Up @@ -36,12 +41,26 @@ export async function GET(request: NextRequest) {
redirectUri,
});
const response = NextResponse.redirect(url);
const returnTarget = normalizeSourceControlOAuthReturnTarget(
request.nextUrl.searchParams.get('redirectTo'),
);
response.cookies.set('roomote-gitea-oauth-state', state, {
httpOnly: true,
sameSite: 'lax',
secure: webEnv.R_APP_URL.startsWith('https://'),
path: '/api/source-control/gitea/oauth',
maxAge: 600,
maxAge: SOURCE_CONTROL_OAUTH_COOKIE_MAX_AGE,
});
response.cookies.set(
getSourceControlOAuthReturnCookieName('gitea'),
returnTarget ?? '',
{
httpOnly: true,
sameSite: 'lax',
secure: webEnv.R_APP_URL.startsWith('https://'),
path: '/api/source-control/gitea/oauth',
maxAge: SOURCE_CONTROL_OAUTH_COOKIE_MAX_AGE,
},
);
return response;
}
70 changes: 55 additions & 15 deletions apps/web/src/app/api/source-control/gitea/oauth/callback/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,47 @@ import {
} from '@roomote/gitea';
import { authorize, getCallbackHost } from '@/lib/server';
import { bootstrapWebRuntimeEnv } from '@/lib/server/bootstrap-runtime-env';
import { getSetupBootstrapState } from '@/lib/server/setup-bootstrap-state';
import { syncRepositoriesCommand } from '@/trpc/commands/source-control';
import {
addSourceControlOAuthResult,
getSourceControlOAuthReturnCookieName,
isSetupOAuthReturnTarget,
resolveSourceControlOAuthReturnTarget,
} from '@/lib/server/source-control-oauth-redirect';

export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';

export async function GET(request: NextRequest) {
const webEnv = await bootstrapWebRuntimeEnv();
const callbackOrigin = new URL(getCallbackHost(request)).origin;
const redirect = new URL('/setup', callbackOrigin);
redirect.searchParams.set('step', 'source-control-connect');
const { setupOpen } = await getSetupBootstrapState();
const returnTarget = resolveSourceControlOAuthReturnTarget({
requestedTarget: request.cookies.get(
getSourceControlOAuthReturnCookieName('gitea'),
)?.value,
setupOpen,
});
const redirect = new URL(returnTarget, callbackOrigin);
const response = () => NextResponse.redirect(redirect);
const clearCookies = (result: NextResponse) => {
result.cookies.set('roomote-gitea-oauth-state', '', {
httpOnly: true,
sameSite: 'lax',
secure: webEnv.R_APP_URL.startsWith('https://'),
path: '/api/source-control/gitea/oauth',
maxAge: 0,
});
result.cookies.set(getSourceControlOAuthReturnCookieName('gitea'), '', {
httpOnly: true,
sameSite: 'lax',
secure: webEnv.R_APP_URL.startsWith('https://'),
path: '/api/source-control/gitea/oauth',
maxAge: 0,
});
return result;
};
const authResult = await authorize();
const state = request.nextUrl.searchParams.get('state');
const expectedState = request.cookies.get('roomote-gitea-oauth-state')?.value;
Expand All @@ -30,7 +61,7 @@ export async function GET(request: NextRequest) {
!code
) {
redirect.searchParams.set('gitea', 'error');
return response();
return clearCookies(response());
}
try {
const [baseUrl, clientId, clientSecret] = await Promise.all([
Expand All @@ -48,19 +79,28 @@ export async function GET(request: NextRequest) {
code,
redirectUri: buildGiteaOAuthRedirectUri(callbackOrigin),
});
redirect.searchParams.set('gitea', 'connected');
redirect.searchParams.set('sync', '1');
if (!isSetupOAuthReturnTarget(returnTarget)) {
const syncResult = await syncRepositoriesCommand(authResult, {
provider: 'gitea',
});
if (!syncResult.success) {
throw new Error(syncResult.error);
}
}
const resultTarget = addSourceControlOAuthResult(
returnTarget,
'gitea',
'connected',
);
redirect.href = new URL(resultTarget, callbackOrigin).href;
} catch (error) {
console.error('[Gitea OAuth] callback failed', error);
redirect.searchParams.set('gitea', 'error');
const resultTarget = addSourceControlOAuthResult(
returnTarget,
'gitea',
'error',
);
redirect.href = new URL(resultTarget, callbackOrigin).href;
}
const result = response();
result.cookies.set('roomote-gitea-oauth-state', '', {
httpOnly: true,
sameSite: 'lax',
secure: webEnv.R_APP_URL.startsWith('https://'),
path: '/api/source-control/gitea/oauth',
maxAge: 0,
});
return result;
return clearCookies(response());
}
Loading
Loading