20220223のReactに関する記事は13件です。

【React-Rouer】S3上でreact-router-domを利用したSPAで404エラー

問題 Reactのページ遷移で404になる。 対応方法 S3単体でできます。 amazon s3から → {バケット名}  →「プロパティ」   →「静的ウェブサイトホスティング」(ページの下の方)    →「編集」     →「静的ウェブサイトホスティング」に"index.html"を記載 その他 apacheなどでも同じ問題が発生しますが、以下で表示されるようになります。 sudo vi /etc/httpd/conf.d/httpd.conf #ErrorDocument 404 /missing.html ErrorDocument 404 /index.html apache再起動する 以上
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactで覚えておきたいもの

環境まわり系 yarnで実行 yarn start サーバを起動する npmで実行 PORT=3001 npm start ポートを指定しながら実行できる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NotionをCMSにしたブログ公開サービスを作る

背景と実施したこと NotionAPIを活用して、Notionで記事を書き、その内容をWebアプリ上に公開するシステムを作りました。 APIから取得した情報の型定義の仕方や取得した情報をマークダウンを加味した内容にレンダリングする方法など、けっこう学びがあったの実装の全体の流れをサマりつつポイントをまとめておきます。 なお、Notion APIは執筆した2022/02/23の時点でBeta版です。 それゆえにところどころ苦しいところがあるのですが、おそらく正式版がリリースされる頃には使い勝手が良くなっていると思われます。 ※自分の技術力不足も大いにある… 使用している技術のver情報は以下の通りです。 next: 12.0.7 react: 17.0.2 TailwindCSS: 3.01 typescript: 4.5.4 参考にした記事 Notion APIをTypeScriptで使ったときのAnyを苦しみながらも撲滅しました How the Notion API Powers My Blog 全体概要 フロントエンド 構成は記事一覧を表示するページと記事の詳細をレンダリングするページの2ページです。 一覧ページ、詳細ページともにAPIで内容を取得するためダイナミックルーティングでページを作っています(詳細は後述) 記事詳細ではAPIで取得した内容をマークダウンで描画されているのと同じようにスタイリングする必要があり、この部分が結構苦労しました。 ###バックエンド(APIでの情報取得など) SSG(Static Site Generator)を実現するため、getStaticPropsの中でAPIを呼び出し、その内容を必要に応じて加工してフロントに渡す、ということをしています。 また、ダイナミックルーティングでのURLをgetStaticPathsで生成するためこのメソッドの中でもAPIを呼び出しています。 細かい実装の流れ 記事一覧のページと記事一覧の取得部分を作る 記事一覧の情報取得部分 まずはAPIで記事一覧を取得し、それを一覧ページに渡してあげます。 PerNumは1ページに表示する記事の数です。環境変数とかに埋め込んでもいいと思いますが、ここではハードコードしています。 初期表示は必ず1ページ目なので、1番目から6番目の記事でsliceしています。 ※要素数とperNumで割り算をしてページ数を出しますが、割り切れない場合も当然あるのでその点は注意。 index.tsx import BlogMain from '../components/blogMain'; import PageFooter from '../components/footer'; import PageHeader from '../components/header'; import Hero from '../components/hero'; import getBlogAll from '../lib/post'; import type { Article } from '../components/blogMain'; export async function getStaticProps() { const res = await getBlogAll(); const perNum = 6; const result = res.results.slice(0, perNum); const itemLength = res.results.length; let indexNum = Math.floor(itemLength / perNum); if (itemLength % perNum != 0) { indexNum += 1; } let indexList: string[] = []; for (let i = 0; i < indexNum; i++) { indexList.push((i + 1).toString()); } return { props: { result, indexList, }, }; } type Props = { result: Article[]; indexList: string[]; }; export default function Index(props: Props): JSX.Element { return ( <> <PageHeader></PageHeader> <main> <Hero /> <BlogMain items={props.result} indexList={props.indexList} /> </main> <PageFooter></PageFooter> </> ); } APIで記事を取得するのにgetBlogAllというメソッドを呼び出していますが中身は以下のようなイメージです。 Notio上でpublishのチェックボックスがONになったもののみ表示されるようにしています。 並び順もここで指定してしまったほうが良いでしょう。 NotionAPIのデータ構造などが不明な人は以下の記事も合わせて御覧ください。 Notion APIのデータ構造を実際にAPIを叩きながら理解する post.tsx const dbId = 'XXXXXXXXXXXXXXXXX'; import { Client } from '@notionhq/client'; export default async function getBlogAll() { const notion = new Client({ auth: process.env.NOTION_API_KEY, }); return await notion.databases.query({ database_id: dbId, filter: { or: [ { property: 'publish', checkbox: { equals: true, }, }, ], }, sorts: [ { property: 'create_time', direction: 'ascending', }, ], }); } ちなみにNotionのDBは↓以下のように作っています。 記事一覧の描画部分 あとは渡ってきたデータを適当にスタイリングしてあげましょう。 個人的に苦労したのはTypescriptでの型定義でした。 最初は import type { QueryDatabaseResponse } from '@notionhq/client/build/src/api-endpoints.d'; こんな感じで、SDKに同梱されている型定義を流用しようと思ったのですが、どうにもここがいけてなく結局自作しました。 APIで取得できる情報は膨大ですが、各コンポーネントで使う情報だけを都度定義付けしてあげるというのが良い気がします。 タグを表示するのにmapで二重ループを行っている点も要注意です。 blogMain.tsx(一部省略) interface Props { items: Article[]; indexList: string[]; } type Tag = { name: string; }; export type Article = { id: string; properties: { tag: { multi_select: { name: string; }[]; }; create_time: { created_time: string }; title: { title: { text: { content: string }; }[]; }; image: { files: { file: { url: string; }; name: string; }[]; }; }; }; export default function BlogMain(props: Props): JSX.Element { const items = props.items; const indexList = props.indexList; const lastIndex = indexList[indexList.length - 1]; const router = useRouter(); const currentPath = router.asPath; const currentPathId = currentPath.slice(-1); return ( <section> <div className='p-4 bg-light-blue'> <div className='pt-8 ml-10 text-lg font-bold'> {`${currentPathId}/${lastIndex}`}ページ </div> <div className='p-20 mx-auto min-w-[95%]'> <div className='grid grid-cols-2 grid-rows-2 gap-4 md:grid-cols-3'> {items.map((item: Article, Index: number) => ( <div className='bg-white rounded ' key={Index}> <Link href={`/blog/${item.id}`}> <a> <div className='pt-2 text-center border-b-2 hover:opacity-50'> <Image src={item.properties.image.files[0].file.url} alt={item.properties.image.files[0].name} width={400} height={200} ></Image> </div> </a> </Link> <h6 className='ml-2'>{sliceDate(item.properties.create_time.created_time)}</h6> <h3 className='ml-2 text-2xl font-bold text-left text-secondary-black'> {item.properties.title.title[0].text.content} </h3> <div className='flex my-2'> {item.properties.tag.multi_select.map((tag: Tag, Index_tag: number) => ( <div className='px-2 ml-2 rounded border' key={Index_tag}> {tag.name} </div> ))} </div> </div> ))} </div> </div> <Pagination indexList={indexList}></Pagination> </div> </section> ); } 記事詳細部分を作る 記事詳細の情報取得部分 一覧ページの <Link href={`/blog/${item.id}`}> この部分から各記事の詳細に入ります。 ここでも同じようにgetStaticPropsを使うのですが、その前にgetStaticPathsでルート(遷移先のURL)を作ります。 該当部分は以下です。 blog/[id].tsx export async function getStaticPaths() { const res = await getBlogAll(); const items = res.results; const paths = items.map((items) => ({ params: { id: items.id, }, })); return { paths, fallback: false }; } その後getStaticPropsでパラメータとしてクリックした記事のIDをもらい、 そのIDを引数にして記事の詳細情報を取得します。 Promise.allで 記事の詳細情報を描画するためのgetChildBlock タイトルやサムネなどを取得するためのgetPageInfo 記事詳細からも次の記事や前の記事に移れるように記事の全量を取得するgetBlogAllを並列で動かします。 さらにgetChildBlockに子要素がある場合はその要素も取得したいためfor文でループを掛けています。 ただし、これでは2階層までしか取得できません。。。 ホントは子要素がなくなるまで無限ループをしたいのですが、一旦諦めました。ここを上手にかけそうな人はぜひコメントください。 blog/[id].tsx export const getStaticProps = async (context: Context) => { const id = context.params.id; const res = await Promise.all([getChildBlock(id), getPageInfo(id), getBlogAll()]) .then((result) => { return result; }) .catch((result) => { console.log('失敗', result); return result; }); let list = []; for (let i = 0; i < res[0].results.length; i++) { if (res[0].results[i].has_children === true) { list.push(await getChildBlock(res[0].results[i].id)); let child1: any = await getChildBlock(res[0].results[i].id); console.log(child1); res[0].results[i].children = child1; for (let t = 0; t < child1.results.length; t++) { if (child1.results[t].has_children === true) { let child2 = await getChildBlock(child1.results[t].id); res[0].results[i].children.results[t].children = child2; } } } } return { props: { id, res, }, }; }; let child1: any = await getChildBlock(res[0].results[i].id); (このanyも消したかったのですが、ちょっとうまくいかず。。。orz APIの呼び方が良くないのかも知れない) 記事詳細の描画部分 APIで取得した内容をpropsで各コンポーネントに渡してあげます。 記事詳細は 記事のサムネやタイトルなど表示するBlogTitle h1~h3のブロックを集めたBlogSummary 本文を描画するBlogArticle が主なコンポーネントです。 サマリー用のデータはfilterで抽出しています。 ※「prose prose-2xl」見慣れないCSSが適用されているかと思いますが、ここは後述します。 blog/[id].tsx export default function BlogPage(props: Props): JSX.Element { const results = props.res[0].results; const pageInfo = props.res[1]; const blogList = props.res[2]; const indexNum = props.id; const sumItems = results.filter((item: SumItems) => { if (item.type === 'heading_1' || item.type === 'heading_2' || item.type === 'heading_3') { return true; } }); const items = listCheck(results); return ( <> <PageHeader></PageHeader> <div className='flex flex-row-reverse justify-between mx-auto min-w-[95%] bg-light-blue'> <div className='basis-3/12 '> <BlogSummary items={sumItems} pageInfo={pageInfo}></BlogSummary> </div> <article className='prose prose-2xl'> <div className='basis-8/12 my-4 bg-white rounded'> <div className='ml-4'> <div className='mx-2 mt-4'> <BlogTitle pageInfo={pageInfo}></BlogTitle> </div> <div className='mx-2 mt-2'> <BlogArticle articleItems={items}></BlogArticle> </div> <div className='py-4 mt-2'> <ArticleFooter blogList={blogList} indexNum={indexNum}></ArticleFooter> </div> </div> </div> </article> <div className='basis-1/12'> <div className='mx-2 mt-4 text-center'> <Image src='/images/twitter_b.png' alt='twitter' width={50} height={50}></Image> </div> <div className='mx-2 mt-4 text-center'> <Image src='/images/facebook_b.png' alt='facebook' width={50} height={50}></Image> </div> </div> </div> <PageFooter></PageFooter> </> ); } また、記事の中にlistがある場合 リスト1 リスト2 リスト3 数字付きリスト1 数字付きリスト2 数字付きリスト3 こんな雰囲気でレンダリングしたいのですが、APIの結果だけではリストの最初と最後の要素が判断できません。 (ここが個人的には一番使いづらい) したがってリストがある場合は前後のブロックのtypeを比較してリストの最初と最後を判断し、最後の要素に今までのリストの中身をすべて詰める、みたいなことをしています。 とはいえ、これも完璧ではなく子要素がある場合などは描画できません。。。 ここも良い書き方があればコメントください。 export function listCheck(item: ArticleItems[]) { let listArray = []; for (let i = 0; i < item.length; i++) { const type = item[i].type; let typePre; let typeNext; if (i === 0) { typePre = 'Nan'; } else if (i === item.length - 1) { typeNext = 'Nan'; } else { typePre = item[i - 1].type; typeNext = item[i + 1].type; } if (type === 'numbered_list_item' && type != typePre) { const listItem = item[i].numbered_list_item.text[0].plain_text; listArray.push(listItem); item[i].numbered_list_item.items = undefined; } else if (type === 'numbered_list_item' && type === typePre && type === typeNext) { const listItem = item[i].numbered_list_item.text[0].plain_text; listArray.push(listItem); item[i].numbered_list_item.items = undefined; } else if (type === 'numbered_list_item' && type != typeNext) { const listItem = item[i].numbered_list_item.text[0].plain_text; listArray.push(listItem); item[i].numbered_list_item.items = listArray; listArray = []; } else if (type === 'bulleted_list_item' && type != typePre) { const listItem = item[i].bulleted_list_item.text[0].plain_text; listArray.push(listItem); item[i].bulleted_list_item.items = undefined; } else if (type === 'bulleted_list_item' && type === typePre && type === typeNext) { const listItem = item[i].bulleted_list_item.text[0].plain_text; listArray.push(listItem); item[i].bulleted_list_item.items = undefined; } else if (type === 'bulleted_list_item' && type != typeNext) { const listItem = item[i].bulleted_list_item.text[0].plain_text; listArray.push(listItem); item[i].bulleted_list_item.items = listArray; listArray = []; } } return item; } BlogArticleの中は以下の様なイメージです。 これだけみるとスタリングが全然足りていないように思うかも知れませんが 実は@tailwindcss/typographyというpluginを追加しています。 このプラグインを適用すると、HTMLのタグをみてそれっぽいCSSをいい感じにあててくれます。 Notionから取得できるtypeは限られていますが、それでも数は多いのでこちらを使うのCSSの記述は多少簡単になるかもしれません。 ※さきほどの「prose prose-2xl」はこのプラグインを適用するための記述です。 参考:@tailwindcss/typography article export default function BlogArticle(props: Props): JSX.Element { const items = props.articleItems; return ( <> {' '} {items.map((item: ArticleItems, index: number) => { return render(item, index); })} </> ); } export function render(item: ArticleItems, index: number) { const tag = item.type; if (tag === 'heading_1') { const value = item.heading_1.text[0].plain_text; return ( <h1 key={index} id={item.id} className='mt-2'> {value} </h1> ); } else if (tag === 'heading_2') { const value = item.heading_2.text[0].plain_text; return ( <h2 key={index} id={item.id} className=''> {value} </h2> ); } else if (tag === 'heading_3') { const value = item.heading_3.text[0].plain_text; return ( <h3 key={index} id={item.id} className=' bg-zinc-200'> {value}{' '} </h3> ); } else if (tag === 'paragraph') { const value = item.paragraph.text[0]; if (value != null) { return ( <p key={index} className='my-2'> {item.paragraph.text.map((item: Text, index: number) => item.href != null ? ( <a href={item.href} key={index}> {item.plain_text} </a> ) : ( <span key={index}>{item.plain_text}</span> ), )} </p> ); } } else if (tag === 'bulleted_list_item' && item.bulleted_list_item.items) { const items = item.bulleted_list_item.items; return ( <ul className='my-2 list-disc list-inside' key={index}> {items.map((item: string, index: number) => ( <li key={index}>{item}</li> ))} </ul> ); } else if (tag === 'numbered_list_item' && item.numbered_list_item.items) { const items = item.numbered_list_item.items; return ( <ol className='my-2 list-decimal list-inside' key={index}> {items.map((item: string, index: number) => ( <li key={index}>{item}</li> ))} </ol> ); } else if (tag === 'image') { const url = item.image.file.url; return <Image key={index} src={url} alt='twitter' width={800} height={400}></Image>; } else if (tag === 'divider') { return <div key={index} className=' min-w-[95%] border-t-4'></div>; } else if (tag === 'quote') { const value = item.quote.text[0].plain_text; return ( <blockquote key={index} className=''> {value} </blockquote> ); } else if (tag === 'code') { const value = item.code.text[0].plain_text; return ( <pre key={index}> <code>{value}</code> </pre> ); } else if (tag === 'table') { if (item.table.has_column_header === true && item.table.has_row_header === true) { return ( <table key={index}> <tbody> {item.children.results.map((tr: Tr, index: number) => ( <tr key={index} className={` ${index === 0 ? 'bg-yellow-100' : ''}`}> {tr.table_row.cells.map((td: Td, index: number) => ( <td key={index} className={` ${index === 0 ? 'bg-yellow-100' : ''}`}> {td[0].plain_text}{' '} </td> ))} </tr> ))} </tbody> </table> ); } else if (item.table.has_column_header === true && item.table.has_row_header === false) { return ( <table key={index}> <tbody> {item.children.results.map((tr: Tr, index: number) => ( <tr key={index} className={` ${index === 0 ? 'bg-yellow-100' : ''}`}> {tr.table_row.cells.map((td: Td, index: number) => ( <td key={index}>{td[0].plain_text} </td> ))} </tr> ))} </tbody> </table> ); } else if (item.table.has_column_header === false && item.table.has_row_header === true) { return ( <table key={index}> <tbody> {item.children.results.map((tr: Tr, index: number) => ( <tr key={index}> {tr.table_row.cells.map((td: Td, index: number) => ( <td key={index} className={` ${index === 0 ? 'bg-yellow-100' : ''}`}> {td[0].plain_text}{' '} </td> ))} </tr> ))} </tbody> </table> ); } else { return ( <table key={index}> <tbody> {item.children.results.map((tr: Tr, index: number) => ( <tr key={index}> {tr.table_row.cells.map((td: Td, index: number) => ( <td key={index}>{td[0].plain_text} </td> ))} </tr> ))} </tbody> </table> ); } } } 記事のフッターを作成する 最後にArticleFooter の部分を解説します。 以下の画像のように、記事ん最後に次または前の記事に遷移できるようにしたいと思います。 といってもそこまで難しいことは必要ありません。 コンポーネントに対して渡すのは現在のページIDと記事の一覧の2つの情報です。 記事一覧のリストと現在のページIDと比較し、一致した前後のページの情報を取得します。 IDの部分をダイナミックルーティングの引数として渡してあげれば終了です。 articleFooter export default function ArticleFooter(props: ArticleFooterProps): JSX.Element { const blogList = props.blogList.results; const indexNum = props.indexNum; const pageIndex = getPageIndex(indexNum, blogList); return ( <div className='flex justify-between items-center mt-10 text-base'> <div className='px-5 w-1/12 text-white bg-primary-green rounded-l'>{'<'}</div> {pageIndex.preId === '' ? ( <div className='w-4/12'></div> ) : ( <div className='w-4/12 text-center'> {' '} <Link href={`/blog/${pageIndex.preId}`}> <a className='hover:opacity-50'>{pageIndex.preTitle} </a> </Link> </div> )} <div className='px-4 w-2/12 text-center text-white bg-primary-green'> {' '} <Link href={`/`}> <a className='hover:opacity-50'>一覧ページ </a> </Link> </div> {pageIndex.nextId === '' ? ( <div className='w-4/12'></div> ) : ( <div className='w-4/12 text-center'> {' '} <Link href={`/blog/${pageIndex.nextId}`}> <a className='hover:opacity-50'>{pageIndex.nextTitle} </a> </Link> </div> )} <div className='px-5 mr-2 w-1/12 text-white bg-primary-green rounded-r'>{'>'}</div> </div> ); } export function getPageIndex(id: string, list: BlogList[]) { let pageIndex = { preId: '', preTitle: '', nextId: '', nextTitle: '', }; const articleId = id; const blogList = list; for (let i = 0; i < blogList.length; i++) { if (blogList[i].id === articleId && i === 0) { pageIndex.preId = ''; pageIndex.nextId = blogList[i + 1].id; pageIndex.nextTitle = blogList[i + 1].properties.title.title[0].plain_text; } else if (blogList[i].id === articleId && i === blogList.length - 1) { pageIndex.preId = blogList[i - 1].id; pageIndex.preTitle = blogList[i - 1].properties.title.title[0].plain_text; pageIndex.nextId = ''; } else if (blogList[i].id === articleId) { pageIndex.preId = blogList[i - 1].id; pageIndex.preTitle = blogList[i - 1].properties.title.title[0].plain_text; pageIndex.nextId = blogList[i + 1].id; pageIndex.nextTitle = blogList[i + 1].properties.title.title[0].plain_text; } } return pageIndex; } まとめ ポイントに絞って全体の流れを記載してみましたが、いかがでしたでしょうか? それっぽい動きにはなりましたが、以下は引き続きの課題として時間があるときに改善していきたいと思います。 リストの子要素の取得・描画 無駄にAPIを呼んでいる箇所がありそう(ブログリストの取得処理などは1回で済ませられそう) 記事内での画像の描画(大きさを指定しているので元画像のサイズによっては歪む) 画像のExpire Date問題(APIで取得した画像にアクセスできる期限が決まっている) Notionで記事を上げた際に再度ビルドが必要(SSGなので当たり前っちゃ当たり前) 一部は先日リリース発表された新機能で解決できそうですが、果たして… 参考:Next.js 12.1 is now available とはいえNotionのAPIを使えるとできることの幅が増えるのは事実なので、どなたかの参考になれば幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS Amplify React アプリで Azure AD のカスタムドメインと SSO してみた

はじめに エンタープライズ向けに AWS Amplify を採用すると、Azure AD や Octa のような Identity Provider(IdP)とのシングルサインオン要件が必ずと言っていいほど挙がります。Azure AD と Amazon Cognito の公式ドキュメントや様々なブログに手順はまとまっていますが、いざ実装しようとすると手順が多く、結構手間が掛かります。せっかくの機会ですので、キャプチャ多めで手順を丁寧にまとめておきたいと思います。皆さんのお役に立てば幸いです。 ソーシャルサインインの場合 Amplify UI の Authenticator コンポーネントを利用することで、ソーシャルサインイン(Amazon, Apple, Facebook, Google)には socialProviders パラメータ指定だけで対応可能です。これはありがたいですね、、、。しかし、SAML の場合は、そうもいきません。 ソーシャルサインインはパラメータ指定のみ! import { Authenticator } from "@aws-amplify/ui-react"; import "@aws-amplify/ui-react/styles.css"; function App() { return ( <Authenticator socialProviders={["amazon", "apple", "facebook", "google"]}> {({ signOut, user }) => ( <main> <h1>Hello {user.username}</h1> <button onClick={signOut}>Sign out</button> </main> )} </Authenticator> ); } export default App; Amplify UI の Authenticator コンポーネントは、以下のように表示されます。現在のバージョンは、ヘッダやフッタを component パラメータで指定することで拡張可能となり、だいぶ使い勝手が良くなりました。例えば、ヘッダに独自の「Sign In with Azure AD」ボタンを追加し、Auth.federatedSignIn(); を呼び出すことも可能です。しかし、Amplify サインイン、Cognito サインイン、Microsoft サインインと 3 つのサインイン UI を経由するのは、少々手間です。そもそも、Amazon/Apple/Facebook/Google or Cognito という選択肢を与えたくありません。エンタープライズアプリケーションのため Azure AD 一択で良いのです。 やりたいこと やりたいことは至ってシンプルで、「Azure AD との SSO でのみサインイン可能な Amplify アプリ」 の実現です。また先述したように、可能な限り 「いきなり SSO」 に近づけることです。 本記事は、以下の記事の続編となります。やりたいことの骨子は同じなので、大変参考になりました。ただ、Amazon Cognito の画面レイアウトが New Interface に刷新され、ガラッと変わってしまったので、対象項目を探すのが大変でした、、、。その意味でも、丁寧にキャプチャを取得しました。少なくとも New Interface が維持されるであろう数年間は、役に立つと思います。 Azure ADをAWS Cognito Federated IdPに追加して、Amplify+Vueアプリでログインする(模索中) 前提条件 環境まわりをまとめておきます。 ソフトウェアおよびライブラリ バージョン node 16.0.0 yarn 1.22.11 @aws-amplify/cli 7.6.15 react ^17.0.2 typescript ^4.4.2 aws-amplify ^4.3.14 @aws-amplify/ui-react ^2.5.0 SSO したい Amplify アプリを用意する まず、最終的に Azure AD と SSO したい AWS Amplify アプリを用意します。手順は以下の通りです。 React アプリを作成する Amplify ライブラリを追加する AWS Amplify を初期化する(適用する) Amplify ホスティングを追加する React アプリの起動確認を行う Amazon Cognito 認証を追加する React アプリを作成する アプリ名 sso-with-aad で React アプリを作成します(適宜読み替えてください)。TypeScript にしておきます。 % yarn create react-app sso-with-aad --template typescript Amplify ライブラリを追加する プロジェクトフォルダに移動し、aws-amplify、@aws-amplify/ui-react を追加しておきます。 % cd sso-with-aad % yarn add aws-amplify @aws-amplify/ui-react AWS Amplify を初期化する(適用する) amplify init コマンドを実行します。インタラクティブなパラメータ指定は、すべてデフォルトのままです。 % amplify init Amplify ホスティングを追加する amplify add hosting コマンドで Amplify ホスティングを追加します。単なる検証ですので、Manual deployment を選択します(CI/CD を設定すると、git push トリガーで自動パブリッシュされるようになります)。次に amplify publish コマンドでクラウド環境に手動デプロイします。CloudFormation が実行され、3分ほど待つと Cognito ユーザプールが作成されます。CloudFormation が実行され、1分もかからずに Web アプリがデプロイされます。 項目 入力値 Select the plugin module to execute Hosting with Amplify Console (Managed hosting with custom domains, Continuous deployment) Manual deployment % amplify add hosting % amplify publish React アプリの起動確認を行う React テンプレートのままですが、念のため、起動確認しておきましょう。URL は amplify publish コマンドのコンソール出力の最後の行に表示されています。後続の手順で必要となるため、URL を控えておきます。アクセスすると、毎度お馴染みの React テンプレートアプリが正常に起動されました。まだ、フロントエンド側に一切の認証設定をしていないので、サインイン不要のシンプルなアプリとなっています。「とりあえずローカルで」という方は、yarn start して、http://localhost:3000 にアクセスしてください。 Amazon Cognito 認証を追加する amplify add auth コマンドで認証を追加します。数が多いですが、以下のパラメータを指定します。最初に Manual configuration を選択することで、OAuth フローの追加設定が可能になります。次に amplify push コマンドでクラウド環境にデプロイします。CloudFormation が実行され、3分ほど待つと Cognito ユーザプールが作成されます。 項目 入力値 Do you want to use the default authentication and security configuration? Manual configuration Select the authentication/authorization services that you want to use User Sign-Up & Sign-In only (Best used with a cloud API only) Provide a friendly name for your resource that will be used to label this category in the project <提案値のまま> Provide a name for your user pool <提案値のまま> How do you want users to be able to sign in? Email Do you want to add User Pool Groups? No Do you want to add an admin queries API? No Multifactor authentication (MFA) user login options OFF Email based user registration/forgot password Enabled (Requires per-user email entry at registration) Specify an email verification subject Your verification code Specify an email verification message Your verification code is {####} Do you want to override the default password policy for this User Pool? No What attributes are required for signing up? Email Specify the app's refresh token expiration period (in days) 30 Do you want to specify the user attributes this app can read and write? No Do you want to enable any of the following capabilities? (指定しない) Do you want to use an OAuth flow? Yes What domain name prefix do you want to use? <提案値のまま> Enter your redirect signin URI <Amplify アプリのURL(最後は"/")> Enter your redirect signout URI <Amplify アプリ のURL(最後は"/")> Select the OAuth flows enabled for this project Authorization code grant Select the OAuth scopes enabled for this project Phone, Email, OpenID, Profile, aws.cognito.signin.user.admin Select the social providers you want to configure for your user pool (何も選択しない) Do you want to configure Lambda Triggers for Cognito? No % amplify add auth % amplify push Azure AD に独自ドメインを追加する 元々、検証用に Route 53 でドメインを取得済みでしたので、今回はそちらのドメインを利用します。仮に、mydomain.com とします。 カスタムドメインを追加する TXT レコードを追加する(Amazon Route 53) 追加したカスタムドメインを検証する カスタムドメインをプライマリドメインに変更する カスタムドメインを追加する Azure Services の中から「Azure Active Directory」を選択します。 サイドメニューの「Custom domain names」を選択すると、デフォルトドメインである <Your Domain>outlook.onmicrosoft.com が表示されます。ツールバーの「Add custom domain」を選択し、画面左のポップアップの「Custom domain name」フィールドに独自ドメイン名を入力し、「Add domain」ボタンを選択します。 カスタムドメインが追加され、検証のための手順が指示されます。「Verify」ボタンを選択する前に、以下の TXT レコードを DNS に登録する必要があります。次の手順で、Route 53 の TXT レコードを編集します。 キー 値 Record type TXT Alias or host name @ Destinationor points to address MS=<ms から始まる一意な値> TTL 3600 TXT レコードを追加する(Amazon Route 53) DNS に Route 53 を利用しているので、AWS マネジメントコンソールから Route 53 を開き、対象の Hosted Zone(仮に mydomain.com)を選択します。既に Alias or host name(Route 53 画面上は「Record name」フィールド)が空の TXT レコードがありましたので、「Value」に MS キーを追加しました。TTL は、指示された 3600 とは異なりますが、元の 300 のままとします。 キー 値 Record name (@ ではなく空で OK) Record type TXT Value (二行目にダブルクォーテーションで囲んだ)"MS=<ms から始まる一意な値>" TTL (指示された3600と違いますが既存レコードのまま)300 追加したカスタムドメインを検証する TXT レコードを変更(あるいは追加)して即座に「Verify」ボタンを選択すると、以下のエラーメッセージが表示されて、検証に失敗します。 Could not find the DNS record for this domain.DNS changes may take up to 72 hours to propagate. Please try again later. 5分ほど伝播するのを待ってからリトライすると、以下の通り検証に成功しました。 カスタムドメインをプライマリドメインに変更する 続けて、カスタムドメイン(仮に mydomain.com)をプライマリドメインに変更しておきます。検証成功直後の画面のツールバーから 「Make primary」を選択します。確認ダイアログに対して、Yes 応答します。 以上の手順で、独自ドメイン(仮に mydomain.com)が Azure AD に検証済みのプライマリドメインとして登録されました。 Azure AD に検証用ユーザを追加する 独自ドメイン(仮に mydomain.com)に検証用ユーザを追加します。アプリケーションのサインイン時に指定するユーザです。 検証用ユーザを追加する メールアドレスを追加する 検証用ユーザを追加する Azure Active Directory のデフォルトディレクトリ(仮に mydomain.com)を選択し、サイドメニューの「Users」を選択します。 現在、Azure 管理者の Microsoft アカウントのみ登録されています。ツールバーの「New user」を選択し、検証用ユーザを追加していきます。 「Create user」をチェックし、以下の項目を入力して 「Create」ボタンを選択します。 キー 値 User name <myalias>@<mydomain.com> Name フルネーム Identity セクションの下に、Password/Groups and roles/Settings/Job info セクションが続きますが、特に変更しません。 以上の手順で、検証用ユーザが作成されました。続けて、メールアドレスを追加していきます。 メールアドレスを追加する 後ほど必要になるので、検証用ユーザのメールアドレスを設定しておきます。ユーザ一覧から検証用ユーザを選択し、ツールバーの「Edit」を選択します。 Contact info セクションにある Email フィールドに連絡可能なメールアドレスを入力し、ツールバーの「Save」ボタンを選択します。 以上で、検証用ユーザに連絡可能なメールアドレスが設定されました。 Amplify アプリを Azure AD エンタープライズアプリケーションとして設定する 最初の手順で作成した検証用 Amplify アプリを、Azure AD のエンタープライズアプリケーションとして設定します。 エンタープライズアプリケーションを追加する エンタープライズアプリケーションにユーザを追加する エンタープライズアプリケーションに Single sign-on (SSO) を設定する エンタープライズアプリケーションの Single sign-on (SSO) をテストする エンタープライズアプリケーションを追加する Azure Active Directory のデフォルトディレクトリ(仮に mydomain.com)を選択し、サイドメニューの「Enterprise applications」を選択します。 ツールバーの「New application」を選択します。 ツールバーの「Create your own application」を選択します。 アプリケーション名とアプリケーションタイプを入力・選択し、「Create」ボタンを選択します。アプリケーション名は少し長いですが、AWS Amplify React (SSO with Azure AD) としました。 キー 値 What's the name of your app? <アプリケーション名> What are you looking to do with your application? Integrate any other application you don't fine in the gallery (Non-gallery) 以上の手順で、エンタープライズアプリケーション(AWS Amplify React (SSO with Azure AD))が登録されました。 エンタープライズアプリケーションにユーザを追加する サイドメニューから「Users and groups」を選択し、ツールバーの「Add user/group」をクリックをします。 Users の None selected を選択します。 追加したいユーザを選択し、「Select」ボタンを選択します。 1 user selected. と表示されていることを確認し、「Assign」ボタンを選択します。 以上の手順で、AWS Amplify React (SSO with Azure AD) エンタープライズアプリケーションにユーザが割り当たりました。 エンタープライズアプリケーションに Single sign-on (SSO) を設定する サイドメニューから「Single sign-on」を選択します。4 つの方式から「SAML」を選択します。 Single Sign-On with SAML 設定画面が表示されます。ステップ 1:Basic SAML Configuration の「Edit」を選択します。 Identifier と Reply URL を入力し、「Save」ボタンを選択します。Cognito の User Pool ID と Cognito Domain (Hosted UI および Auth 2.0 エンドポイント)は、前の手順で控えておいたものを指定します。 項目 入力値 Identifier (Entity ID) urn:amazon:cognito:sp:<Cognito User Pool ID> Reply URL (Assertion Consumer Service URL) https://<Cognito Domain>.auth.ap-northeast-1.amazoncognito.com/saml2/idpresponse ステップ 3:SAML Signing Certificate の App Federation Metadata Url の値をコピーしておきます。後ほど、Cognito 側で設定します。 エンタープライズアプリケーションの Single sign-on (SSO) をテストする ステップ 5:Test single sign-on with AWS Amplify React (SSO with Azure AD) の「Test」ボタンを選択します。 次の手順のために、Microsoft 謹製の My Apps Secure Sign-in Extension ブラウザ拡張をインストールしておきます(「Sign in as current user」を選択する場合は不要です)。今回は、Azure 管理者の Microsoft アカウントでサインインした状態で、検証用ユーザのサインインをテストするので、「Sign in as someone else (required browser extension)」を選択します。 「Sign in as someone else (required browser extension)」を選択し、「Test sign in」ボタンを選択します。 先ほど追加した検証用ユーザの Azure AD アカウント(連絡用メールアドレスではなく、myalias@mydomain.com の方です)を入力し、「Next」を選択します。 パスワードを入力し、「Sign in」を選択します。 Azure ADでの認証が OK となり、Reply URL で指定した Cognito エンドポイントにリダイレクトされ、別タブが開きます。パラメータエラーが発生していますが、現時点では無視して構いません。 SSO テストに成功し、以下のメッセージが表示されました。右上の「×」で Test を閉じます。以上の手順で、AWS Amplify React (SSO with Azure AD) エンタープライズアプリケーションの設定が完了しました。続けて、リダイレクト先である Cognito の設定を行います。 Azure AD successfully issued a token (SAML response) to the application (service provider). If you still can’t access the application you need to contact the software vendor and share the information below. Amazon Cognito を設定する いよいよ設定の大詰めです。まず、Cognito から見たときの Federated IdP として Azure AD を追加し、 アイデンティティプロバイダを追加する Cognito Hosted UI を設定する アイデンティティプロバイダを追加する(オーバーライド) 管理コンソールから手でポチポチ設定しても良いのですが、せっかくなのでオーバーライドしてみます。amplify override auth コマンドを実行し、amplify/backend/auth//override.ts を生成します。 % amplify override auth 以下のコードを追加します。Directory ID と Applicaiton ID は、適宜読み替えてください。 override.ts import { AmplifyAuthCognitoStackTemplate } from "@aws-amplify/cli-extensibility-helper"; export function override(resources: AmplifyAuthCognitoStackTemplate) { resources.addCfnResource( { type: "AWS::Cognito::UserPoolIdentityProvider", properties: { ProviderName: "AzureAD", ProviderType: "SAML", UserPoolId: resources.userPool.ref, AttributeMapping: { email: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", }, ProviderDetails: { MetadataURL: "https://login.microsoftonline.com/<Directory ID>/federationmetadata/2007-06/federationmetadata.xml?appid=<Applicaiton ID>", }, }, }, "SAML" ); } amplify push コマンドを実行し、設定変更を反映します。 % amplify push 以上の手順で、AzureAD という名前の SAML アイデンティティプロバイダが追加されました。 オーバーライドコードで指定した Attribute mapping や Metadata document も反映されています。 Cognito Hosted UI を設定する 続けて、App integration タブにある App client list から、Web 向けのアプリケーションクライアント(_app_clientWeb で終わるもの)を選択します。 選択したアプリケーションが表示されますので、下の方にスクロールしていきます。 Hosted UI の「Edit」を選択します。 Identity providers として、先ほど作成した AzureAD のみを選択します。 「Save Changes」ボタンを選択します。以上の手順で、AzureAD がアイデンティティプロバイダとして設定されました。 Amplify アプリに認証を組み込む やっと Amazon Cognito と Azure AD の設定が完了しましたので、いよいよ Amplify アプリに認証を組み込みます。 ソースコードを修正する 修正版の動作確認する フローをまとめておく ソースコードを修正する yarn create react-app で生成されたテンプレートに対して、以下のような修正を加えました。意味もなくダークテーマを適用しているため、少し長くなっていますが、要するに認証されていない場合、Coginito エンドポイントを呼び出すという少々乱暴なコードですが、ご容赦ください。 App.tsx import { useEffect, useState } from "react"; import Amplify, { Auth, Hub } from "aws-amplify"; import { createTheme, defaultTheme, AmplifyProvider, Button, Loader, Text, } from "@aws-amplify/ui-react"; import "@aws-amplify/ui-react/styles.css"; import { CognitoUser } from "@aws-amplify/auth"; import logo from "./logo.svg"; import "./App.css"; import awsconfig from "./aws-exports"; Amplify.configure(awsconfig); const App = () => { const [user, setUser] = useState<CognitoUser | null>(null); const [authInProgress, setAuthInProgress] = useState(false); const theme = createTheme({ name: "dark-mode-theme", overrides: [ { colorMode: "dark", tokens: { colors: { neutral: { // flipping the neutral palette 10: defaultTheme.tokens.colors.neutral[100], 20: defaultTheme.tokens.colors.neutral[90], 40: defaultTheme.tokens.colors.neutral[80], 80: defaultTheme.tokens.colors.neutral[40], 90: defaultTheme.tokens.colors.neutral[20], 100: defaultTheme.tokens.colors.neutral[10], }, black: { value: "#fff" }, white: { value: "#000" }, }, }, }, ], }); useEffect(() => { const unsubscribe = Hub.listen("auth", ({ payload: { event, data } }) => { switch (event) { case "signIn": setUser(data); setAuthInProgress(false); break; case "signOut": setUser(null); setAuthInProgress(false); break; default: } }); Auth.currentAuthenticatedUser() .then((user) => setUser(user)) .catch(() => { window.location.href = `https://${awsconfig.oauth.domain}/login?response_type=${awsconfig.oauth.responseType}&client_id=${awsconfig.aws_user_pools_web_client_id}&redirect_uri=${awsconfig.oauth.redirectSignIn}`; setAuthInProgress(true); }); return unsubscribe; }, []); return ( <AmplifyProvider theme={theme} colorMode="dark"> <div className="App"> <header className="App-header"> {authInProgress ? ( <> <Loader width="5rem" height="5rem" /> <Text>Signing in ...</Text> </> ) : ( <> <img src={logo} className="App-logo" alt="logo" /> <p> Edit <code>src/App.js</code> and save to reload. </p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn React </a> <br /> <Button onClick={() => Auth.signOut()}> Sign Out ({user?.getUsername()}) </Button> </> )} </header> </div> </AmplifyProvider> ); }; export default App; 修正版の動作確認する https://dev.<Applicaiton ID>.amplifyapp.com にアクセスします。一瞬、以下の画面が表示されて、Cognito Hosted UI に遷移します。 Cognito Hosted UI が表示されたら、「AzureAD」ボタンをクリックします。AzureAD は、Cognito の Hosted UI に設定した Identity providers 名です。 既にサインイン済みのため、アカウントを選択します。もし、サインイン済みでない場合は、ユーザ名およびパスワードを入力することになります。 アプリにリダイレクトされ、「Sign Out」ボタンが表示されました。カッコ内は、Cognito User Pool の User name が表示されています。ここで「Sign Out」ボタンを選択すると、Cognito からサインアウトされて、再び Cognito Hosted UI が表示されます。 フローをまとめておく おおまかには、このようなフローとなります(非同期の API コールなどは、端折っています)。 各エンドポイントは以下の通りです。手順は多かったですが、要するに「次にどこへ行けばいいのか」を順番に設定していたことが分かります。 No URL 説明 1 https://dev.<Applicaiton ID>.amplifyapp.com Amplify Hosting エンドポイント(アプリ)。Cognito Hosted UI の Allowed callback URLs および Allowed sign-out URLs に設定されている 2 https://<Cognito Domain>.auth.ap-northeast-1.amazoncognito.com/login?response_type=code&client_id=<Client ID>&redirect_uri=<Redirect URI> Amazon Cognito へのサインイン。未認証の場合、Hosted UI が表示される 3 https://<Cognito Domain>.auth.ap-northeast-1.amazoncognito.com/oauth2/authorize?identity_provider=AzureAD&redirect_uri=<Redirect URI>&response_type=CODE&client_id=<Client ID>&scope=<Scopes> Hosted UI の「Sign In」ボタン選択により、Cognito ユーザプールに Federated IdP として設定した AzureAD 設定に従い、OAuth フローが開始される 4 https://login.microsoftonline.com/<Directory ID>/saml2?SAMLRequest=<Token>&RelayState=<Token> マイクロソフトのサインイン UI が表示される。未認証の場合は、ユーザおよびパスワードを入力。認証済みの場合は、サインイン済みのアカウントリストから選択する。Cognito の SAML IdP として追加した AzureAD の Metadata document に指定した URL から返される XML 内で SingleSignOnService の Location に設定されている 5 https://login.microsoftonline.com/<Directory ID>/login Azure AD へのサインイン。認証に成功した場合、サインイン状態を維持するかどうかを選択する 6 https://<Cognito Domain>.auth.ap-northeast-1.amazoncognito.com/saml2/idpresponse Amazon Cognito へのフェデレーテッドサインイン。アプリへの認可コード(?code=<Grant Code>)付きのアプリ URL が返される。Azure AD エンタープライズアプリケーションの Reply URL (Assertion Consumer Service URL) に設定されている 7 https://dev.<Applicaiton ID>.amplifyapp.com/?code=<Grant Code> Amplify Hosting エンドポイント(アプリ) さいごに AWS Amplify との出会いは遅めだったのですが、幸いエンタープライズ界隈の人間ですので、相対的に早めになっている気がします(良いか悪いかは別として)。AWS Amplify を採用したアジャイルプロジェクトがもたらす破壊的なアジリティは、非常に魅力的です。今後より一層、AWS Amplify のエンタープライズ採用が進むことを大いに期待しています。その意味でも、「エンタープライズあるある」である当記事がお役に立てば幸いです。 「はじめに」で触れたように、本記事は、「Azure ADをAWS Cognito Federated IdPに追加して、Amplify+Vueアプリでログインする(模索中)」の続編となります。前編の投稿から 2 年 7 ヶ月(!)が経過していることに驚きを禁じ得ません。その間に、AWS Amplify も React も大きな進化を遂げ、AWS や Azure も非常に多くの機能が追加・改善されました。少し足を止めて当記事をまとめましたが、引き続き、終わりなきキャッチアップの旅に出たいと思います。 参考リンク Azure ADをAWS Cognito Federated IdPに追加して、Amplify+Vueアプリでログインする(模索中) Amplify UI 公式ドキュメント / Authenticator [Amplify 公式ドキュメント / Auth events] (https://docs.amplify.aws/lib/auth/auth-events/q/platform/js/) Amazon Cognito デベロッパーガイド / ユーザープールへの SAML ID プロバイダーの追加
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vite/React/TypeScriptのプロジェクトを立ち上げるだけ

概要 題名の通り 略手順 initする プロジェクト名を指定(今回はvite-test) reactを選択する react-tsを選択する 指定されたコマンドを打つ 指定されたlocalhostにアクセスする 手順 1 $ npm init vite 2 $ npm init vite ? Project name: vite-test 3 ✔ Project name: … vite-test ? Select a framework: › - Use arrow-keys. Return to submit. vanilla vue ❯ react preact lit svelte 4 ? Select a variant: › - Use arrow-keys. Return to submit. react ❯ react-ts 5-1 : 出力されるコマンド Scaffolding project in /Users/pauli-agile/Documents/Practice/vite-test... Done. Now run: cd vite-test npm install npm run dev 5-2 : 出力されたコマンドを打つ $ cd vite-test $ npm install added 88 packages, and audited 89 packages in 12s 8 packages are looking for funding run `npm fund` for details found 0 vulnerabilities $ npm run dev > ownd_mirror@0.0.0 dev > vite Pre-bundling dependencies: react react-dom react/jsx-dev-runtime (this will be run only when your dependencies or config have changed) vite v2.8.4 dev server running at: > Local: http://localhost:3000/ > Network: use `--host` to expose ready in 389ms. 6 終わり
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

useStateからreact-reduxに置き換える

前提 以前作成した下記記事のHooksを使って置き換えます reduxとtoolkitの導入 yarn add react-redux @types/react-redux @reduxjs/toolkit ファイルの変更と追加 index.tsx import React from "react"; import ReactDOM from "react-dom"; import { Provider } from "react-redux"; import App from "./app"; import { store } from "./redux/store"; ReactDOM.render( <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode>, document.getElementById("root") ); <Provider>でAppを囲う <Provider>:Reduxのstoreを利用できるようにする src > reduxフォルダ > store.ts import { configureStore, combineReducers } from "@reduxjs/toolkit"; export const store = configureStore({ reducer: combineReducers({}), }); export type RootState = ReturnType<typeof store.getState>; export type AppDispatch = typeof store.dispatch; store:stateの保存場所 useStateではpropsでデータをバケツリレーしていたが、reduxではstoreから直接データを渡せる 様々なサイトを見た時にconfigureStoreとcombineReducersのファイルを分けていることがあるが、最初はstore.tsにまとめておく(Reducerが多くなったら切り分ける可能性あり) fetchの置き換え app.tsx useState版 import React, { FC, useEffect, useState } from "react"; import { Content } from "./component/content"; import { Ranking } from "./types/type"; const App: FC = () => { const [ranking, setRanking] = useState<Ranking[]>([]); useEffect(() => { const fetchData = async () => { const response = await fetch("http://localhost:3000/ranking"); const rankingData = await response.json(); setRanking(rankingData); }; fetchData(); }, []); return ( <> <Content ranking={ranking} /> </> ); }; export default App; app.tsx redux置き換え版 import React, { FC, useEffect } from "react"; import { useDispatch } from "react-redux"; import { Content } from "./component/content"; import { ranking } from "./redux/fetch"; const App: FC = () => { const dispatch = useDispatch(); useEffect(() => { dispatch(ranking()); }, []); return ( <> <Content /> </> ); }; export default App; useEffect内にあったfetchを切り分け useStateが必要なし Contentコンポーネントに渡していたデータも必要なし ※fetchの切り分けは下記 redux > fetch.ts import { createAsyncThunk } from "@reduxjs/toolkit"; export const ranking = createAsyncThunk("rankingData", async () => { const response = await fetch("http://localhost:3000/ranking"); const rankingData = await response.json(); return rankingData; }); fetchの際にasyncを使っていたので非同期処理であり、ReduxのデータフェッチとRedux toolkitに記載されていたので使用 createAsyncThunk:非同期処理に対応したActionCreatorを生成する関数 ここまでで確認① app.tsxの<Content>は一旦、<p>TEST</p>に変更 content.tsxのコードは全てコメントアウト yarn mockとyarn startを実行 ブラウザの検証からReduxを開く※google chromの拡張機能Redux DevTools rankingData/fullfilledのaction > payload内にデータが入っていればdispatchしているfetchは成功 Sliceを使ってでStoreにデータを送る redux >  rankingSlice.ts import { createSlice } from "@reduxjs/toolkit"; import { RankingData } from "../types/type"; import { ranking } from "./fetch"; const initialState: RankingData = { ranking: [], }; export const rankingDataSlice = createSlice({ name: "rankingData", initialState, reducers: {}, extraReducers: (builder) => { builder .addCase(ranking.pending, () => { //非同期処理中のロジック }) .addCase(ranking.fulfilled, (state, { payload }) => { //非同期処理成功時のロジック state.ranking = payload; }) .addCase(ranking.rejected, (error) => { //非同期処理失敗時のロジック error; }); }, }); sliceでデータの更新 今回のuseStateの役割 = Slice useState版:ranking = redux版:initialState useState版:setRanking = redux版:extraReducers extraReducers:「外部」アクションを参照することを目的としているため、アクションで生成されたものは使えない reducers:特定のアクションタイプを処理するための関数で、switchのcase文に相当 store.ts import { rankingDataSlice } from "./rankingSlice"; import { configureStore, combineReducers } from "@reduxjs/toolkit"; export const store = configureStore({ reducer: combineReducers({ ranking: rankingDataSlice.reducer }), }); export type RootState = ReturnType<typeof store.getState>; export type AppDispatch = typeof store.dispatch; storeでデータ保管 rankingDataSliceをimport reducerにSliceのデータを追加 ここまでで確認② yarn mockとyarn startを実行 ブラウザの検証からReduxを開く rankingData/fullfilledのstate > ranking > ranking内にデータが入っていればstoreにデータが入っているのでOK! あとはstoreからデータを取り出すだけ! Storeからデータを与える 今回Storeからデータを与えたいのは子であるcontent.tsx content.tsx import React, { FC } from "react"; import { useSelector } from "react-redux"; import { RootState } from "../redux/store"; import { RankingData } from "../types/type"; export const Content: FC<RankingData> = () => { const { ranking } = useSelector((state: RootState) => state.ranking); if (!ranking) throw new Error("rankingがありません"); return ( <> <h1>Hello Hello</h1> <table> <tr> {ranking.map(({ word }) => { return ( <tr> <td key={word}>{word}</td> </tr> ); })} </tr> </table> </> ); }; コメントアウトを消す propsの部分も消す app.tsxの<p>TEST</p>は<Content/>に変更 <Content/>でエラーが出ているのでtype.d.tsを編集(rankingに?をつける) export interface RankingData { ranking?: Ranking[]; } reduxからuseSelectorをimport stateを管理しているstoreからRootStateもimport あとはuseSelectorでrankingのデータを持ってくる rankingを持ってきた時にundefinedの可能性があるのでタイプガードをする 最後に yarn mockとyarn startを実行 ここでmockのデータが表示されていたら成功! 今回はcomponent1つですが複数になった場合や孫のコンポーネントが出てきた場合、useStateであるとpropsのバケツリレーは大変です。 理解するのは難しいですがreduxとtoolkitを使えるようになるとコードがスッキリし、storeから直接データを渡せるので便利です! 参考文献
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelで REST API を実装し Reactと連携したCRUDアプリ作成

業務でReact×Laravelのアプリ作成に関わっているので勉強しています。 今回は学習用のアウトプットです。 Laravel側 ## 環境構築 主にReactとLaravelの連携部分のアウトプットなので環境構築について詳細の説明は省きます。 今回はこちらを記事を参考にさせていただきました。 ## API作成 手順はこちらの記事を参考にさせていただきました。 モデルとマイグレーションの作成 $ php artisan make:model Book --migration 作成されたBookモデルに追記 app/Models/Book.php <?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Book extends Model { use HasFactory; protected $fillable = [ 'title', 'author', ]; } マイグレーションファイル記述 database/migrations/xxxx_xx_xx_xxxxxx_create_books_table.php <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('books', function (Blueprint $table) { $table->id(); $table->string('title'); $table->string('author'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('books'); } }; マイグレーション実行 $ php artisan migrate コントローラーの作成 $ php artisan make:controller BookController --api ルーティング設定 routes/api.php <?php use App\Http\Controllers\BookController; use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; /* |-------------------------------------------------------------------------- | API Routes |-------------------------------------------------------------------------- | | Here is where you can register API routes for your application. These | routes are loaded by the RouteServiceProvider within a group which | is assigned the "api" middleware group. Enjoy building your API! | */ Route::middleware('auth:sanctum')->get('/user', function (Request $request) { return $request->user(); }); Route::apiResource('/books', BookController::class); ルーティング確認 php artisan route:list 結果 GET|HEAD / .................................................................................................................................... POST _ignition/execute-solution ............................. ignition.executeSolution › Spatie\LaravelIgnition › ExecuteSolutionController GET|HEAD _ignition/health-check ......................................... ignition.healthCheck › Spatie\LaravelIgnition › HealthCheckController POST _ignition/update-config ...................................... ignition.updateConfig › Spatie\LaravelIgnition › UpdateConfigController GET|HEAD api/books ......................................................................................... books.index › BookController@index POST api/books ......................................................................................... books.store › BookController@store GET|HEAD api/books/{book} .................................................................................... books.show › BookController@show PUT|PATCH api/books/{book} ................................................................................ books.update › BookController@update DELETE api/books/{book} .............................................................................. books.destroy › BookController@destroy GET|HEAD api/user ............................................................................................................................. GET|HEAD sanctum/csrf-cookie ...................................................................... Laravel\Sanctum › CsrfCookieController@show コントローラーにメソッド追加 app/Http/Controllers/BookController.php <?php namespace App\Http\Controllers; use App\Models\Book; use Illuminate\Http\Request; class BookController extends Controller { /** * Display a listing of the resource. * * @return \Illuminate\Http\Response */ public function index() { $books = Book::all(); return response()->json( $books, 200 ); } /** * Store a newly created resource in storage. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function store(Request $request) { $book = Book::create($request->all()); return response()->json( $book, 201 ); } /** * Update the specified resource in storage. * * @param \Illuminate\Http\Request $request * @param int $id * @return \Illuminate\Http\Response */ public function update(Request $request, $id) { $update = [ 'title' => $request->title, 'author' => $request->author ]; $book = Book::where('id', $id)->update($update); $books = Book::all(); if ($book) { return response()->json( $books , 200); } else { return response()->json([ 'message' => 'Book not found', ], 404); } } /** * Remove the specified resource from storage. * * @param int $id * @return \Illuminate\Http\Response */ public function destroy($id) { $book = Book::where('id', $id)->delete(); if ($book) { return response()->json([ 'message' => 'Book deleted successfully', ], 200); } else { return response()->json([ 'message' => 'Book not found', ], 404); } } } フォームリクエストバリデーション作成 $ php artisan make:request StoreBook app/Http/Requests/StoreBook.php <?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Contracts\Validation\Validator; use Illuminate\Http\Exceptions\HttpResponseException; use Illuminate\Validation\ValidationException; class StoreBook extends FormRequest { /** * Determine if the user is authorized to make this request. * * @return bool */ public function authorize() { return true; } /** * Get the validation rules that apply to the request. * * @return array */ public function rules() { return [ 'title' => 'required', 'author' => 'required', ]; } public function messages() { return [ 'title.required' => 'タイトルが未入力です', 'author.required' => '著者が未入力です', ]; } protected function failedValidation(Validator $validator) { $errors = (new ValidationException($validator))->errors(); throw new HttpResponseException(response()->json([ 'message' => 'Failed validation', 'errors' => $errors, ], 422, [], JSON_UNESCAPED_UNICODE)); } } curlコマンドでAPIを叩いてみる 新規作成 $ curl -X POST http://localhost:80/api/books -d 'title=羅生門&author=芥川龍之介' すべてのレコードを確認 $ curl http://localhost:80/api/books/ React側 参考記事 新規プロジェクト作成 $ npx create-react-app {プロジェクト名} --template typescript Appコンポーネントを以下の記述に変更 import axios from "axios"; import { useEffect, useState } from "react"; import "./App.css"; type Book = { id: number; title: string; author: string; }; export const App = () => { const [books, setBooks] = useState<Book[]>([ { id: 0, title: "", author: "", }, ]); useEffect(() => { axios .get("http://localhost:80/api/books/") .then((response) => setBooks(response.data)) .catch((error) => console.log(error)); }, []); const [title, setTitle] = useState<string>(""); const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => { setTitle(e.target.value); }; const [author, setAuthor] = useState<string>(""); const handleAuthorChange = (e: React.ChangeEvent<HTMLInputElement>) => { setAuthor(e.target.value); }; const createNewBook = (): void => { axios .post("http://localhost:80/api/books/", { title: title, author: author, }) .then((response) => { setBooks([...books, response.data]); }) .then(() => { setAuthor(""); setTitle(""); }) .catch((error) => { console.log(error); }); }; const deleteBook = (id: number) => { axios .delete(`http://localhost:80/api/books/${id}`) .then((response) => { console.log(response); setBooks(books.filter((book) => book.id !== id)); }) .catch((error) => console.log(error)); }; const modifyBook = (id: number) => { axios .patch(`http://localhost:80/api/books/${id}`, { title: title, author: author, }) .then((response) => { setBooks(response.data); }) .then(() => { setAuthor(""); setTitle(""); }) .catch((error) => console.log(error)); }; return ( <> <ul> {books.map((book) => ( <> <li key={book.id}> タイトル:{book.title} 作者:{book.author} <button onClick={() => deleteBook(book.id)}>削除</button> <button onClick={() => modifyBook(book.id)}>更新</button> </li> </> ))} </ul> <label> タイトル: <input value={title} onChange={handleTitleChange} /> </label> <label> 作者: <input value={author} onChange={handleAuthorChange} /> </label> <br /> <button onClick={createNewBook}>作成</button> </> ); }; exportを変更したので他のファイルでエラーが発生した場合は適宜修正。 以上になります。 型の定義方法等はまだまだ勉強中ですので修正点ありましたら教えていただきたく思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

『inputタグのvalue属性をe.target.valueを用いてuseStateに管理する』の意味が分からなかったので調べた

そもそも下記のコードは何がしたいのか? inputに入力した値を取り出してuseStateで管理したい。 なぜなら入力した値を用いて投稿だったり編集ができるから。 HogeHoge const HogeHoge = () => { const [neko, setNeko] = useState({ title: '' }) const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { setNeko({ ...neko, [e.target.name]: e.target.value, }) } return ( <div> <label htmlFor="title">タイトル</label> <input type="text" name="title" id="title" onChange={handleChange} value={neko.title} /> </div> ) } export default HogeHoge まずuseStateを使ってnekoの初期値を設定しています。 ここではキーがtitleで値が空のオブジェクトを、nekoの初期値として設定しています。 HogeHoge const [neko, setNeko] = useState({ title: '' }) そしてsetNekoという更新関数を用いてnekoの値を更新 ここでnekoの初期値をスプレッド構文で展開しています。つまり ...nekoによって title: '' が取り出されています。その空の値がsetNekoのオブジェクトに代入されています。要するにオブジェクトがクローンされています。 そして[e.target.name]: e.target.valueの値が挿入されていきます HogeHoge const [neko, setNeko] = useState({ title: '' }) const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { setNeko({ ...neko, [e.target.name]: e.target.value, }) } オブジェクトクローンの例 const foo = { a: 1, b: 2 }; const bar = { ...foo }; // => { a: 1, b: 2 } [e.target.name]: e.target.valueとは inputタグのnameの値と、inputタグのvalueの値を指します。 今回はnameがtitleで、valueがneko.titleです。こうすることでinputタグが2個3個と増えてもname属性とvalue属性を参照しているので、入力値をstateで管理できます。 また、value属性は入力した値そのものを指します。つまり今回はnekoというオブジェクトのtitleの値が、inputタグのvalue属性になります。 HogeHoge <input type="text" name="title" id="title" onChange={handleChange} value={neko.title} /> nekoの出力をコンソールで見てみます HogeHoge const [neko, setNeko] = useState({ title: '' }) console.log(neko) const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { setNeko({ ...neko, [e.target.name]: e.target.value, }) } 最初は空の文字列ですが文字を入力すると、setNekoにvalue属性の値が渡ってきて、nekoが更新されていきます。 このnekoをAPI及びDBの通信に適用することで投稿機能や編集機能が作れます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Firebase】Firestoreからランダムでデータを取得するにはどうすればいいの?

背景 個人アプリを作っている中でFirestoreからランダムで5個だけデータを取得する実装をしたかったのですが、調査を進めていくとそのような処理はFirebase側では用意されていないようなので、参考資料を元にReact, Firebaseで実装しました。 実装までに躓いたことを主にコードの解説をしていきます。 実装したコード hooks/useRandomDocument.tsx import { useEffect, useState } from "react"; import { projectFirestore } from "../firebase/config"; import { ProductDoc } from "../@types/stripe"; import { useCookies } from "react-cookie"; export const useRandomDocument = () => { const [documents, setDocuments] = useState<Array<ProductDoc>>([]); const [cookies] = useCookies(["random"]); const randomIndex = Number(cookies.random); useEffect(() => { let indexs: Array<string> = []; let randomDocument: Array<ProductDoc> = []; async function handleAsync() { while (randomDocument.length < 5) { const queryIndex = String(Math.floor(Math.random() * randomIndex + 1)); // DBの中に格納されている商品数以下の数字をランダムで出力する if (!indexs.includes(queryIndex)) { indexs = [...indexs, queryIndex]; const productsRef = await projectFirestore.collection("products"); const snapshot = await productsRef .orderBy("metadata.random") .startAt(queryIndex) .endAt(queryIndex) .get(); // startAtとendAtを同一に指定することでユニークな結果を出力できる const data = snapshot.docs.map((doc) => { return { ...(doc.data() as ProductDoc), id: doc.id }; }); randomDocument = [...randomDocument, ...data]; if (randomDocument.length === 5) { setDocuments(randomDocument); } } } } handleAsync(); }, [randomIndex]); return { documents }; }; 今回の味噌 該当ページに遷移したらuseRandomDocumentを発火させます。 Firestoreからランダムに1個データを取得する処理を5個データがrandomDocumentに溜まるまで繰り返し条件を満たしたらuseStateに入れて処理を終了します。 ■あらかじめ取得するデータにユニークな値randomをつける あらかじめ取得するデータにrandom:<数字>を持たせる必要があります。 この下準備をしておくことで後述するstartAt(queryIndex)とendAt(queryIndex)を同じ値で指定して1個のみ取得することができます。 ■数値をランダムで生成する 先に述べたようにFirestoreからランダムで値を取得できないのでランダムの処理は下記で再現しています。 const queryIndex = String(Math.floor(Math.random() * randomIndex + 1)); ■between条件でクエリしてデータを取得する const snapshot = await productsRef .orderBy("metadata.random") .startAt(queryIndex) .endAt(queryIndex) .get(); 「あらかじめ取得するデータにつけたrandom」と「ランダムで生成した数値」が合致するデータを取得します。 本来ならstartAt(2月1日)とendAt(2月28日)にして1ヶ月の値をまとめてとってくるのがユースケースらしいですが、startAtとendAtの引数を同じ値にすることで1個だけデータを取得できます。 特定のデータを1個取得するならcollection().doc()の印象でしたがstartAtとendAtの方法もあるのですね。目から鱗でした。 さいごに ということで、Firestoreから値をランダムで取得する方法について述べてきましたが、(今回の場合は)5回Firestoreへアクセスするのでパフォーマンスは落ちますし、その際のLoadingなどのUI・UX対策は必須になります。 もし他に良い方法があればご教示いただけますと幸いです。 参考URL
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React・GraphQL(urql)でのOpenID Connect認証

最近フロントエンド側にも手を出している @nyasba です。 Reactでクライアント側のOpenIDConnectでの認証機能を実装することがあったので知見をまとめます。Reactの場合、ライブラリの組み合わせ部分まで含めてまとまった情報がなく少し戸惑いました。 やったこと クライアント側でOpenIDConnectの認可コードフローでの認証機能を実現し、 取得したアクセストークンを用いてGraphQLリクエストでの認証を行えるようにすること。 ※GraphQLサーバ側の実装は含みません。 シーケンス mermaid形式の元ファイル sequence.mmd sequenceDiagram participant u as ユーザ participant c as クライアントアプリ<br>(React) participant a as 認証サーバ<br>(Keycloak) participant s as GraphQLサーバ<br>(今回は対象外) u ->>+ c: 認証が必要な画面にアクセス c -->>- u: 認証画面にリダイレクト u ->>+ a: 認証画面表示 a -->>- u: 認証完了、redirect_uriにリダイレクト u ->>+ c: /callback?認可コード c ->>+ a: 認可エンドポイント a -->>- c: アクセストークンを取得 c ->>+ s: 認証ヘッダーを付与してGraphQLエンドポイントを呼び出す s ->> s: 認証情報の検証 s ->>+ a: ユーザエンドポイント a ->>- s: ユーザ情報 s ->> s: ユーザ情報をもとに処理 s ->>- c: レスポンス c ->>- u: 画面表示 実施環境 React v17 React Router v6 react-oidc-context (OIDCクライアント) urql (GraphQLクライアント) @urql/exchange-auth (urqlの認証機能拡張) keycloak(認証サーバ) 認証サーバ側の設定(Keycloak) dockerなどでkeycloakを立ち上げる Realmを登録 Clientを登録 accesTypeはpublicとする webOrigin・redirect_uriの設定を行う(webOriginはCORSのための設定) ログイン用のユーザを作成する クライアントで実装した内容まとめ 以下のように段階的に実装していったので、その順に解説します。 認証が必要なルーティングの場合に認証画面にリダイレクトするようにする 認証後のコールバックを受け取る アクセストークンを取得する GraphQLの通信時にAuthorizationヘッダーにセットする 1. 認証が必要なルーティングの場合に認証画面にリダイレクトするようにする まずはRouterに AuthProviderを設定します。react-oidc-contextの公式ドキュメントにある通り、 oidcCondigで、認証サーバに関する設定を行います。 認証が必要なページへのルーティングについては RouteAuthGuardを使ってコンポーネントをラップすることで未認証時のリダイレクトを実現しています。 App.tsx <Router> <AuthProvider {...oidcConfig}> <Provider value={graphqlClient}>  <Routes> <Route path="/auth" element={ <RouteAuthGuard component={<AuthPage />} /> } /> <Route path="/noauth" element={<NoAuthPage />} /> <Route path="/callback" element={<AuthCallback />} /> </Routes>   </Provider> </AuthProvider> </Router> oidc-config.ts const oidcConfig = { authority: 'https://{認証サーバのホスト}/auth/realms/{Realm}', client_id: '{ClientId}', redirect_uri: `{クライアントアプリのホスト}/callback`, }; 認証が必要なページに被せるガードコンポーネントです。未認証の場合、 auth.signinRedirect()でログイン画面に飛ばしています。 実際のリダイレクトURLは下記の通り。何もしなくてもPKCEにも対応できているようです https://{認証サーバのドメイン}/auth/realms/{Realm}/protocol/openid-connect/auth ?client_id={client_id} &redirect_uri={redirect_uri} &response_type=code &scope=openid &state=bd7035c6e9854471940797510552b5f5 &code_challenge=ahn3Nr1Xj7rDZMZ4fJ10FV-OIYc5NeY_4HhanuOlFJY &code_challenge_method=S256 &response_mode=query RouteAuthGuard.tsx import { ReactNode, VFC } from 'react'; import { useAuth } from 'react-oidc-context'; import { useLocation } from 'react-router-dom'; export const PATH_LOCAL_STORAGE_KEY = 'path'; type Props = { component: ReactNode; }; /** * 認証ありのRouteガード */ export const RouteAuthGuard: VFC<Props> = ({ component }) => { // 認証情報にアクセスするためのhook const auth = useAuth(); const location = useLocation(); if (auth.isLoading) { return <Loading />; // ローディングコンポーネント } if (auth.error) { throw new Error('unauthorized'); // 要件に応じて実装を見直すこと } // 認証されていなかったらリダイレクトさせる前にアクセスされたパスを残しておく if (!auth.isAuthenticated) { localStorage.setItem(PATH_LOCAL_STORAGE_KEY, location.pathname); void auth.signinRedirect(); } return <>{component}</>; }; export default RouteAuthGuard; 2. 認証後のコールバックを受け取る oidcCondigで設定した通り、 認証が完了すると/callbackにリダイレクトして戻ってきます。 あらかじめ Routerに定義していた通り、AuthCallbackコンポーネントが呼ばれ、元々アクセスしようとしていた画面が表示されることになります。 ここでauth.isLoadingの判定がない場合、認可コードからアクセストークンを取得する処理などの間に再度ルーティング判定が行われ、無限リダイレクトが発生することになりますのでご注意ください AuthCallback.tsx import { VFC } from 'react'; import { Navigate } from 'react-router-dom'; import { PATH_LOCAL_STORAGE_KEY } from 'auth/RouteAuthGuard'; import { useAuth } from 'react-oidc-context'; /** * 認証後のCallbackエンドポイント */ export const AuthCallback: VFC = () => { const auth = useAuth(); if (auth.isLoading) { return <Loading />; // ローディングコンポーネント } // ログイン前にアクセスしようとしていたパスがあれば取得してリダイレクト const redirectLocation = localStorage.getItem(PATH_LOCAL_STORAGE_KEY); localStorage.removeItem(PATH_LOCAL_STORAGE_KEY); return <Navigate to={redirectLocation ?? '/'} replace />; }; export default AuthCallback; 3. アクセストークンを取得する これで認証が完了した状態になりますので、アクセストークンをSessionStorageから取得できるようになります。アクセストークンとその有効期限をurqlで利用するので取得できるようにしておきます。 なお、アクセストークン自体は有効期限が短かったとしても、ライブラリ側で自動的にリフレッシュトークンを用いたアクセストークンの再取得が行われるため、リフレッシュ処理を意識する必要はありませんでした。 AuthToken.ts export type AuthToken = { token: string; expiredAt: number | undefined; }; export const getAuthToken = (): AuthToken | null => { const oidcData = sessionStorage.getItem( `oidc.user:${oidcConfig.authority}:${oidcConfig.client_id}`, ); if (!oidcData) { return null; } const authUser = User.fromStorageString(oidcData); return !authUser ? null : { token: authUser.access_token, // アクセストークン expiredAt: authUser.expires_at, // 有効期限 }; }; 4. GraphQLの通信時にAuthorizationヘッダーにセットする 次は、urqlの公式ドキュメントに従い、GraphQL通信時の認証ヘッダーの追加を行います。基本的には公式に従って作っています。 ここでは、fetchExchangeより前にauthExchangeの設定をしておく必要があります(...defaultExchangesを使っている場合も、順序の制御が必要となるため、この書き方に変える必要があります) graphql-client.ts /** * GraphQL Client設定 */ const graphqlClient = createClient({ url: '/graphql', exchanges: [ dedupExchange, cacheExchange, authExchange(authConfig), // fetchExchangeの前に設定する必要がある fetchExchange ], }); authConfigの中身はこちら。 graphql-auth.ts import { AuthConfig } from '@urql/exchange-auth'; import { makeOperation } from 'urql'; import { getAuthToken, AuthToken } from 'auth/AuthToken'; import { getUnixTime } from 'date-fns'; export const authConfig: AuthConfig<AuthToken> = { // 認証ヘッダーにアクセストークンを追加 addAuthToOperation: ({ authState, operation }) => { if (!authState || !authState.token) { return operation; } const fetchOptions = typeof operation.context.fetchOptions === 'function' ? operation.context.fetchOptions() : operation.context.fetchOptions || {}; return makeOperation(operation.kind, operation, { ...operation.context, fetchOptions: { ...fetchOptions, headers: { ...fetchOptions.headers, Authorization: `Bearer ${authState.token}`, }, }, }); }, // 認証が間も無く切れるかどうかをauthTokenの有効期限から判定する willAuthError: ({ authState }) => { if (!authState) { return true; } if ( authState.expiredAt && authState.expiredAt < getUnixTime(new Date()) ) { return true; } return false; }, // 認証に失敗したかどうかをGraphQLのレスポンスから判定する didAuthError: ({ error }) => error.graphQLErrors.some((e) => e.extensions?.code === 'FORBIDDEN'), // 認証情報を取得する getAuth: ({ authState }): Promise<AuthToken | null> => { if (!authState) { return new Promise((resolve) => resolve(getAuthToken())); } return new Promise((resolve) => resolve(null)); }, }; export default authConfig; これでurqlによるGraphQL通信でAuthorizationヘッダーにアクセストークンをセットすることができました。 おわり。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactでアプリを作成しました【11】【Sliding Login & Register Form】

##環境の準備 ①ターミナルでreactアプリケーションを作成する。 $ npx create-react-app <プロジェクト名> % cd <プロジェクト名> % npm start ##コンポーネント・ファイル構成 src ├── components ├── LoginRegister.css ├── LoginRegister.js └──.js ├── App.js ├── App.css ├── index.js ├── .env ##参考サイト [React Sliding Login & Register Form] (https://www.youtube.com/watch?v=1HqLemkphI4&t=934s)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS API Gateway - サーバーレス構成のフロントエンドから API Gateway へのアクセスを IAM で制御する

はじめに バックエンドのサービス間認証は以下の IAM と API Gateway を使用したアクセス制御で良いですが、フロントエンドから APサーバー(BFF) を挟まず、直接 API Gateway にリクエストを行うサーバレスの要件が上がってきました。いいや、降りてきました。 その場合、 cognito を使用して AWS リソースへのアクセス許可を取得するのが一般的なようで、そのとき行った設定をメモしておきます。 やりたいこと フロントエンドから直接… cognito(IDプール) から未認証ユーザーのロールを付与してもらい、ゲストユーザーとして未認証ユーザー用の API が実行できる。 cognito 認証後、認証済みユーザー用の API が実行できる。 Cognito 1. ユーザープールの作成 デフォルトの設定を使用します。 2. アプリクライアントの追加 「クライアントシークレットの生成」を解除し、SRP認証を有効にします。 USER_SRP_AUTH SRP 認証に関しては以下の記事で投稿してたりします。 3. IDプールの作成 未認証のユーザーに対してもアクセス制御(許可)を行いたいので 「認証されていない ID に対してアクセスを有効にする」 をチェック。 また、認証プロバイダーに先程作成したユーザープールを設定します。 API Gateway 1. API を作成 「REST API」 を選択。 2.リソースの作成 未認証ユーザーに公開する/public 認証済みユーザーに公開する/private を同じ設定で作成します。 フロントエンドから直接リクエストするので CORS を有効にしておきます。 3. HTTPメソッドの作成 どちらもモックデータをを返すように設定しておきます。 4. モックレスポンスの設定 /public と /private どちらかを判別できるレスポンスにしておきます。 5. 動作確認 一度デプロイして動作確認をしてみます。何も設定していないのでどちらにもリクエストが通ります。 6. アクセス制御の設定 未認証ユーザーに公開する/public 認証済みユーザーに公開する/private どちらも同じく 「AWS IAM」 を設定します。 7. 動作確認 デプロイし直します。 著名が付与されていないリクエストがブロックされることが確認できます。 IAM 1. IAM ロールの確認 ID プールを作成したときに一緒に作った 「認証されていないロール」 「認証されたロール」 に対して API Gateway のアクセス許可を設定していきます。 設定されている IAM ロールは ID プールの管理画面 「ID プールの編集」 から確認できます。 2. IAM ポリシーの設定 「認証されていないロール」 は /public に 「認証されたロール」 は /public /privateの両方に対して以下のように設定していきます。 ※ インラインポリシー、ビジュアルエディタ どちらも 「認証されたロール」 に対して設定を行ったときのキャプチャです。 ビジュアルエディタから登録する場合は以下のように フロントエンド フロントエンドを作成していきます。 $ npx create-react-app aws-hello --template typescript 著名の作成 がとても面倒ですが aws-amplifyのパッケージを使用することで、その工程を意識せずに認証を行うことができます。 $ npm i aws-amplify 力尽きました。 長々と書いていて力尽きました。笑 amplify と React.js を使用した SRP 認証の実装は以下を参照していただくと… ログイン前は IDプールから未認証の IAM ロールが付与され /public のみ実行でき、 ログイン後は認証済みの IAM ロールが付与されて /public と /private どちらの URL もフロントエンドから直接リクエストできるようになります。 アドレスバーなどから /public を実行しても未認証用の IAM ロールが付与されていないため、リクエストは通りません。 そして、ログインするためのユーザーは cognito の管理画面で作っておきましょう。 まとめ AWS の認証をうまく使って、機能開発から認証を切り離して開発効率上げたいですね ?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

もうHTML/CSSは書きたくない

もうHTML/CSSは書きたくない 「もうHTML/CSSは書きたくない」とちょっとタイトルで釣りましたが、最後まで読んでくれると嬉しいです! 結論から言うと、Amplify StudioとFigmaを連携させて、Reactのコンポーネントを作成すると言う記事です。 StudioとFigmaを連携させる機能は、2021年の12月に発表されました。 最小限のプログラミングでFigmaからフルスタックのReactアプリを実現 実際に、インターンシップで触れる機会があったので、触れた感想や結果を書きたいと思います。 先に感想を Amplify StudioとFigmaを連携させる方法は、僕以外の人も書いているので、僕の感想を先に書きます。 感想だけでいいと言う方は、ここで離脱しても大丈夫です! 僕のプロフィール エンジニアインターンとして5ヶ月経ち、インターンではReactとAWSを使って開発中。 クラウドプラクティショナー取得済み。 実際にやってみた感想 ・ Figmaを完璧に使えこなせていないと厳しい Figmaの方で雑にデザインを作っていると、いざReactでUIコンポーネントとして利用したときに、ずれが起きたり、画面サイズが変わったときにデザインが崩れたりします。また、Amplify Studioでコンポーネントとして出力させるとこうなるんだ!ということもわかっておくと、進めやすいと思います。 ・ 全部のコンポーネントを置き換える必要は特にない 一応プレビューの段階のため、できないこともあるようです。また、すでにコンポーネントがあるなら、わざわざ置き換えるメリットはないと思います。そのため、新しい比較的小さいコンポーネントを作成する際に少し使ってみるとかはありかもです。 ・ Amplify Studioの操作が難しい これについては、慣れろよとしか言いようがないのですが、DynamoDBからデータを引っ張ってこれたり、型定義ができたりします。AWSの機能は大体そうですが、慣れないと複雑だなーと感じることがあります。 先にFigmaのリンクを取得する 以下Figmaの画面の右上にある、青いShareボタンを押すと、モーダルが出てきます。 そのモーダルの左下にあるCopy Linkを押して、Figmaのリンクを取得します。 以下の画像はサンプルのため、デザインを作成していません Amplify Studioにログインする ここから先は、GraphQL APIバックエンドとReactフロントエンドを持つ、サンプルReactアプリケーションをデプロイしていることを前提に、書き進めていきます。 Amplifyがわからない方は、Amplify SNS Workshopを参考にしてください。 まずはマネジメントコンソールにログインして、Amplifyコンソールにある作成したアプリケーションを開きます。 その後、左のメニューのアプリの設定から、Amplify Studio Settings(設定)を選択します。 すると、以下の画面が出てくるので、Enable Amplify Studioをオンにして、右側にあるInvite usersを押します。 そこで、自分のメールアドレスを打ち、届いたメールに書かれているUDとパスワードで、Amplify Studioにログインします。 Amplify StudioとFigmaを連携させる ログインして、以下のような画面が開ければ、右のメニューにあるUI Libraryを押します。 その後、画面が変わり、真ん中にGet startedがあるので、押します。 すると、上で取得したリンクを記入する欄があるので、そこにペーストして、Continueを押します。 これで連携が完了です。 ReactのUIコンポーネントとして使えるようにする。 Figmaと連携して、右上のFigmaのマークの隣にあるSync with Figmaのボタンを押すと、Figmaで作成したコンポーネントが出てきます。 そこで、以下の画像の前に、RejectもしくはAcceptボタンが出てくるので、必要なコンポーネントのみAcceptします。 そうすると、以下の画像のようになります。 そして、この画像の右少し上にあるConfigureボタンを押すと、画面が少し変わり、下の方に</> Get component codeボタンが出てきます。 それを押すと、ReactのUIコンポーネントとして使えるようにする方法が出てきます。 詳しくは進めるとわかるので、ここではざっとだけ説明すると、amplify pullして、jsxファイルが作成されるため、それを別のファイルからインポートして利用します。 まとめ ここでは、最短でFigmaからReactのコンポーネントを作成する方法を書きました。 Amplify Studioには、まだまださまざまな機能が存在するため、ぜひ試してみてください。 また、もし間違えていることや追加コメントがあれば、教えてください!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む