Skip to content

Commit dee80ff

Browse files
sumi-0011claude
andauthored
feat: 데스크톱 앱 로그인 핸드오프 페이지 추가 (/auth/desktop) (#385)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent eb69d85 commit dee80ff

4 files changed

Lines changed: 177 additions & 1 deletion

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { cookies } from 'next/headers';
2+
import { redirect } from 'next/navigation';
3+
import { css } from '_panda/css';
4+
5+
import { getServerAuth } from '@/auth';
6+
import { buildDesktopCallbackUrl, isValidDesktopRedirect } from '@/constants/desktopAuth';
7+
import { COOKIE_KEY } from '@/constants/storage';
8+
import { DEFAULT_LOCALE } from '@/i18n/routing';
9+
10+
export default async function DesktopAuthEntryPage({
11+
searchParams,
12+
}: {
13+
searchParams: Promise<{ redirect_uri?: string; state?: string }>;
14+
}) {
15+
const { redirect_uri, state } = await searchParams;
16+
17+
if (!redirect_uri || !state || !isValidDesktopRedirect(redirect_uri)) {
18+
return (
19+
<main className={mainCss}>
20+
<h1 className={headingCss}>잘못된 요청</h1>
21+
<p className={descCss}>
22+
{!redirect_uri || !state
23+
? '필수 파라미터(redirect_uri, state)가 누락되었습니다.'
24+
: 'redirect_uri가 허용 범위를 벗어났습니다. GitAnimals 데스크톱 앱을 통해 다시 시도해주세요.'}
25+
</p>
26+
</main>
27+
);
28+
}
29+
30+
const session = await getServerAuth();
31+
if (session?.user?.accessToken) {
32+
redirect(buildDesktopCallbackUrl(redirect_uri, session.user.accessToken, state));
33+
}
34+
35+
const locale = cookies().get(COOKIE_KEY.locale)?.value ?? DEFAULT_LOCALE;
36+
const params = new URLSearchParams({ redirect_uri, state }).toString();
37+
redirect(`/${locale}/auth/desktop?${params}`);
38+
}
39+
40+
const mainCss = css({
41+
backgroundColor: 'white',
42+
w: '100dvw',
43+
h: '100dvh',
44+
display: 'flex',
45+
flexDirection: 'column',
46+
alignItems: 'center',
47+
justifyContent: 'center',
48+
padding: '0 16px',
49+
});
50+
51+
const headingCss = css({
52+
textStyle: 'glyph40.bold',
53+
marginBottom: '12px',
54+
});
55+
56+
const descCss = css({
57+
textStyle: 'glyph16.regular',
58+
});
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
'use client';
2+
3+
import { useEffect } from 'react';
4+
import { useSearchParams } from 'next/navigation';
5+
import { css } from '_panda/css';
6+
7+
import { login } from '@/components/AuthButton';
8+
import { buildDesktopCallbackUrl, isValidDesktopRedirect } from '@/constants/desktopAuth';
9+
import { useClientSession } from '@/utils/clientAuth';
10+
11+
export default function DesktopAuthPage() {
12+
const params = useSearchParams();
13+
const redirectUri = params.get('redirect_uri');
14+
const state = params.get('state');
15+
16+
const { status, data } = useClientSession();
17+
18+
const isValid = isValidDesktopRedirect(redirectUri) && !!state;
19+
20+
useEffect(() => {
21+
if (!isValid) return;
22+
23+
if (status === 'authenticated' && data?.user?.accessToken) {
24+
window.location.replace(buildDesktopCallbackUrl(redirectUri!, data.user.accessToken, state!));
25+
} else if (status === 'unauthenticated') {
26+
login(
27+
`/auth/desktop?redirect_uri=${encodeURIComponent(redirectUri!)}&state=${encodeURIComponent(state!)}`,
28+
);
29+
}
30+
}, [status, isValid, redirectUri, state, data?.user?.accessToken]);
31+
32+
if (!isValid) {
33+
return (
34+
<div className={pageRootCss}>
35+
<div className={cardCss}>
36+
<h1 className={titleCss}>잘못된 요청</h1>
37+
<p className={descCss}>
38+
redirect_uri가 허용 범위를 벗어났거나 필수 파라미터가 누락되었습니다.
39+
</p>
40+
</div>
41+
</div>
42+
);
43+
}
44+
45+
const message =
46+
status === 'authenticated' ? '데스크톱 앱으로 이동합니다…' : '로그인으로 이동합니다…';
47+
48+
return (
49+
<div className={pageRootCss}>
50+
<div className={cardCss}>
51+
<p className={loadingTextCss}>{message}</p>
52+
</div>
53+
</div>
54+
);
55+
}
56+
57+
const pageRootCss = css({
58+
display: 'flex',
59+
flexDirection: 'column',
60+
alignItems: 'center',
61+
justifyContent: 'center',
62+
minHeight: '100vh',
63+
padding: '24px',
64+
bg: 'linear-gradient(180deg, #000 0%, #004875 38.51%, #005B93 52.46%, #006FB3 73.8%, #0187DB 100%)',
65+
color: 'white',
66+
_mobile: {
67+
padding: '16px',
68+
},
69+
});
70+
71+
const cardCss = css({
72+
borderRadius: '16px',
73+
background: 'rgba(255, 255, 255, 0.1)',
74+
backdropFilter: 'blur(7px)',
75+
padding: '40px',
76+
width: 'fit-content',
77+
minWidth: '520px',
78+
maxWidth: '100%',
79+
display: 'flex',
80+
flexDirection: 'column',
81+
alignItems: 'center',
82+
gap: '16px',
83+
_mobile: {
84+
minWidth: '100%',
85+
padding: '24px 16px',
86+
background: 'rgba(255, 255, 255, 0.08)',
87+
},
88+
});
89+
90+
const titleCss = css({
91+
textStyle: 'glyph28.bold',
92+
color: 'white',
93+
_mobile: { textStyle: 'glyph24.bold' },
94+
});
95+
96+
const loadingTextCss = css({
97+
textStyle: 'glyph20.regular',
98+
color: 'white.white_70',
99+
});
100+
101+
const descCss = css({
102+
textStyle: 'glyph16.regular',
103+
color: 'white.white_80',
104+
textAlign: 'center',
105+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export const DESKTOP_REDIRECT_PATTERN =
2+
/^http:\/\/127\.0\.0\.1:(2333[8-9]|2334[0-2])\/auth\/callback$/;
3+
4+
export function isValidDesktopRedirect(url: string | null | undefined): url is string {
5+
return typeof url === 'string' && DESKTOP_REDIRECT_PATTERN.test(url);
6+
}
7+
8+
export function buildDesktopCallbackUrl(redirectUri: string, token: string, state: string): string {
9+
const url = new URL(redirectUri);
10+
url.searchParams.set('token', token);
11+
url.searchParams.set('state', state);
12+
return url.toString();
13+
}

apps/web/src/middleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import createMiddleware from 'next-intl/middleware';
44

55
import { routing } from './i18n/routing';
66

7-
const publicPages = ['/', '/auth', '/event/HALLOWEEN_2024', '/event/CHRISTMAS_2024', '/test/ranking'];
7+
const publicPages = ['/', '/auth', '/auth/desktop', '/event/HALLOWEEN_2024', '/event/CHRISTMAS_2024', '/test/ranking'];
88

99
const intlMiddleware = createMiddleware({
1010
...routing,

0 commit comments

Comments
 (0)