diff --git a/client/package-lock.json b/client/package-lock.json index 5adeb6f..f1c0329 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,6 +8,7 @@ "name": "client", "dependencies": { "@aspida/axios": "^1.14.0", + "@aspida/swr": "^1.14.0", "@aws-amplify/ui-react": "^6.3.0", "@aws-sdk/client-cognito-identity-provider": "^3.645.0", "@fakerjs/word": "^1.0.1", @@ -19,6 +20,7 @@ "next": "^14.2.9", "react": "^18.3.1", "react-dom": "^18.3.1", + "swr": "^2.2.5", "zod": "^3.23.8" }, "devDependencies": { @@ -75,6 +77,15 @@ "axios": "" } }, + "node_modules/@aspida/swr": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@aspida/swr/-/swr-1.14.0.tgz", + "integrity": "sha512-d9xdbI5EqL/tIT5NIMF4OHXeDXcr1NvqLJxcYw9buhhzoBtlsVCuBR+Sih4YTQznFsgKCFylOZbRvCYMJpYmdQ==", + "peerDependencies": { + "aspida": "^1.14.0", + "swr": "" + } + }, "node_modules/@aws-amplify/analytics": { "version": "7.0.47", "resolved": "https://registry.npmjs.org/@aws-amplify/analytics/-/analytics-7.0.47.tgz", @@ -7840,6 +7851,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swr": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.5.tgz", + "integrity": "sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==", + "dependencies": { + "client-only": "^0.0.1", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", diff --git a/client/package.json b/client/package.json index 2bba197..a0afbf2 100644 --- a/client/package.json +++ b/client/package.json @@ -18,6 +18,7 @@ }, "dependencies": { "@aspida/axios": "^1.14.0", + "@aspida/swr": "^1.14.0", "@aws-amplify/ui-react": "^6.3.0", "@aws-sdk/client-cognito-identity-provider": "^3.645.0", "@fakerjs/word": "^1.0.1", @@ -29,6 +30,7 @@ "next": "^14.2.9", "react": "^18.3.1", "react-dom": "^18.3.1", + "swr": "^2.2.5", "zod": "^3.23.8" }, "devDependencies": { diff --git a/client/pages/_app.page.tsx b/client/pages/_app.page.tsx index d9bc716..9d7aad0 100644 --- a/client/pages/_app.page.tsx +++ b/client/pages/_app.page.tsx @@ -33,9 +33,9 @@ function MyApp({ Component, pageProps }: AppProps) { oauth: { domain: 'localhost:5052', scopes: ['openid'], - redirectSignIn: ['http://localhost:5051'], - redirectSignOut: ['http://localhost:5051'], - responseType: 'token', + redirectSignIn: [location.origin], + redirectSignOut: [location.origin], + responseType: 'code', }, }, }, diff --git a/client/pages/index.page.tsx b/client/pages/index.page.tsx index 6c9d42e..3dc31c4 100644 --- a/client/pages/index.page.tsx +++ b/client/pages/index.page.tsx @@ -9,6 +9,8 @@ import { useEffect } from 'react'; import { pagesPath } from 'utils/$path'; import styles from './index.module.css'; +export type OptionalQuery = { code: string; state: string }; + const Home = () => { const { user } = useUser(); const router = useRouter(); diff --git a/client/pages/oauth2/authorize.module.css b/client/pages/oauth2/authorize.module.css index b3fc7df..387fc01 100644 --- a/client/pages/oauth2/authorize.module.css +++ b/client/pages/oauth2/authorize.module.css @@ -4,6 +4,37 @@ margin: 0 auto; } +.userInfo { + display: flex; + gap: 16px; + align-items: center; + padding: 12px 8px; + color: gray; + cursor: pointer; + border-radius: 6px; + transition: 0.2s; +} + +.userInfo:hover { + background: #eee; +} + +.userName { + font-weight: bold; +} + +.userEmail { + font-size: 14px; +} + +.userIcon { + width: 36px; + height: 36px; + background: #aaa; + background-size: cover; + border-radius: 50%; +} + .btn { padding: 6px 16px; font-weight: bold; @@ -14,6 +45,11 @@ border-radius: 6px; } +.desc { + font-size: 14px; + color: gray; +} + .inputLabel { font-size: 14px; color: #777; diff --git a/client/pages/oauth2/authorize.page.tsx b/client/pages/oauth2/authorize.page.tsx index b7ec750..819a591 100644 --- a/client/pages/oauth2/authorize.page.tsx +++ b/client/pages/oauth2/authorize.page.tsx @@ -1,39 +1,61 @@ +import useAspidaSWR from '@aspida/swr'; import type { OAuthConfig } from '@aws-amplify/core'; import word from '@fakerjs/word'; -import { PROVIDER_LIST } from 'common/constants'; +import { APP_NAME, PROVIDER_LIST } from 'common/constants'; import type { MaybeId } from 'common/types/brandedId'; import { Spacer } from 'components/Spacer'; import { useRouter } from 'next/router'; import { useState } from 'react'; +import { apiClient } from 'utils/apiClient'; import { z } from 'zod'; import styles from './authorize.module.css'; export type Query = { redirect_uri: string; - response_type: OAuthConfig['responseType']; client_id: MaybeId['userPoolClient']; identity_provider: string; scope: OAuthConfig['scopes']; state: string; -}; +} & ( + | { response_type: 'code'; code_challenge: string; code_challenge_method: 'plain' | 'S256' } + | { response_type: 'token' } +); -const AddAccount = (props: { provider: string; onBack: () => void }) => { +const AddAccount = (props: { + provider: (typeof PROVIDER_LIST)[number]; + codeChallenge: string; + userPoolClientId: MaybeId['userPoolClient']; + onBack: () => void; +}) => { const [email, setEmail] = useState(''); const [displayName, setDisplayName] = useState(''); const [photoUrl, setPhotoUrl] = useState(''); const data = z .object({ email: z.string().email(), - displayName: z.string(), + name: z.string(), photoUrl: z.literal('').or(z.string().url().optional()), }) - .safeParse({ email, displayName, photoUrl }); + .safeParse({ email, name: displayName, photoUrl }); const setFakeVals = () => { const fakeWord = word({ length: 8 }); setEmail(`${fakeWord}@${props.provider.toLowerCase()}.com`); setDisplayName(fakeWord); }; + const addUser = async () => { + if (!data.success) return; + + await apiClient.public.socialUsers.$post({ + body: { + ...data.data, + photoUrl: photoUrl || undefined, + provider: props.provider, + codeChallenge: props.codeChallenge, + userPoolClientId: props.userPoolClientId, + }, + }); + }; return (