├── .nvmrc ├── .eslintrc.json ├── public ├── favicon.ico ├── GitHub-Mark-32px.png └── GitHub-Mark-Light-32px.png ├── next.config.js ├── next-env.d.ts ├── src ├── server │ ├── routers │ │ ├── _app.ts │ │ └── session.ts │ ├── trpc.ts │ └── context.ts └── utils │ └── trpc.ts ├── pages ├── admin │ └── index.tsx ├── api │ └── trpc │ │ └── [trpc].ts ├── _app.tsx ├── login.tsx ├── profile-ssr.tsx ├── index.tsx └── profile-sg.tsx ├── .env.production ├── .env.development ├── .gitignore ├── lib └── session.ts ├── tsconfig.json ├── middleware.ts ├── components ├── Form.tsx ├── Layout.tsx └── Header.tsx ├── LICENSE ├── README.ko.md ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.15.0 -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parkgang/trpc-iron-session/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/GitHub-Mark-32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parkgang/trpc-iron-session/HEAD/public/GitHub-Mark-32px.png -------------------------------------------------------------------------------- /public/GitHub-Mark-Light-32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parkgang/trpc-iron-session/HEAD/public/GitHub-Mark-Light-32px.png -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: true, 4 | images: { 5 | domains: ["avatars.githubusercontent.com"], 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /src/server/routers/_app.ts: -------------------------------------------------------------------------------- 1 | import { router } from "src/server/trpc"; 2 | import { sessionRouter } from "./session"; 3 | 4 | export const appRouter = router({ 5 | session: sessionRouter, 6 | }); 7 | 8 | export type AppRouter = typeof appRouter; 9 | -------------------------------------------------------------------------------- /pages/admin/index.tsx: -------------------------------------------------------------------------------- 1 | import { NextPage } from "next"; 2 | 3 | const GreetAdmin: NextPage = () =>
Hello Admin!
; 4 | export default GreetAdmin; 5 | 6 | // no need to manually check if admin on each page here 7 | // middleware makes life easy! 8 | -------------------------------------------------------------------------------- /src/server/trpc.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "./context"; 2 | import { initTRPC } from "@trpc/server"; 3 | 4 | const t = initTRPC.context().create(); 5 | 6 | export const router = t.router; 7 | 8 | export const publicProcedure = t.procedure; 9 | -------------------------------------------------------------------------------- /pages/api/trpc/[trpc].ts: -------------------------------------------------------------------------------- 1 | import * as trpcNext from "@trpc/server/adapters/next"; 2 | import { createContext } from "src/server/context"; 3 | import { appRouter } from "src/server/routers/_app"; 4 | 5 | export default trpcNext.createNextApiHandler({ 6 | router: appRouter, 7 | createContext: createContext, 8 | }); 9 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # ⚠️ The SECRET_COOKIE_PASSWORD should never be inside your repository directly, it's here only to ease 2 | # the example deployment 3 | # For production you should use https://vercel.com/blog/environment-variables-ui if you're hosted on Vercel or 4 | # any other secret environment variable mean 5 | 6 | SECRET_COOKIE_PASSWORD=2gyZ3GDw3LHZQKDhPmPDL3sjREVRXPr8 -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # ⚠️ The SECRET_COOKIE_PASSWORD should never be inside your repository directly, it's here only to ease 2 | # the example deployment 3 | # For local development, you should store it inside a `.env.local` gitignored file 4 | # See https://nextjs.org/docs/basic-features/environment-variables#loading-environment-variables 5 | 6 | SECRET_COOKIE_PASSWORD=2gyZ3GDw3LHZQKDhPmPDL3sjREVRXPr8 -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { AppProps } from "next/app"; 2 | import { trpc } from "src/utils/trpc"; 3 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 4 | 5 | function MyApp({ Component, pageProps }: AppProps) { 6 | return ( 7 | <> 8 | 9 | 10 | 11 | ); 12 | } 13 | 14 | export default trpc.withTRPC(MyApp); 15 | -------------------------------------------------------------------------------- /src/server/context.ts: -------------------------------------------------------------------------------- 1 | import * as trpc from "@trpc/server"; 2 | import * as trpcNext from "@trpc/server/adapters/next"; 3 | import { getIronSession } from "iron-session"; 4 | import { sessionOptions } from "lib/session"; 5 | 6 | export async function createContext(opts: trpcNext.CreateNextContextOptions) { 7 | const session = await getIronSession(opts.req, opts.res, sessionOptions); 8 | 9 | return { 10 | session, 11 | }; 12 | } 13 | 14 | export type Context = trpc.inferAsyncReturnType; 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /lib/session.ts: -------------------------------------------------------------------------------- 1 | // this file is a wrapper with defaults to be used in both API routes and `getServerSideProps` functions 2 | import type { IronSessionOptions } from "iron-session"; 3 | 4 | export type User = { 5 | isLoggedIn: boolean; 6 | login: string; 7 | avatarUrl: string; 8 | }; 9 | 10 | export const sessionOptions: IronSessionOptions = { 11 | password: process.env.SECRET_COOKIE_PASSWORD as string, 12 | cookieName: "iron-session/examples/next.js", 13 | cookieOptions: { 14 | secure: process.env.NODE_ENV === "production", 15 | }, 16 | }; 17 | 18 | // This is where we specify the typings of req.session.* 19 | declare module "iron-session" { 20 | interface IronSessionData { 21 | user?: User; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "baseUrl": ".", 21 | "incremental": true 22 | }, 23 | "include": [ 24 | "next-env.d.ts", 25 | "**/*.ts", 26 | "**/*.tsx", 27 | "next.config.js" 28 | ], 29 | "exclude": [ 30 | "node_modules" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import type { NextRequest } from "next/server"; 3 | import { getIronSession } from "iron-session/edge"; 4 | import { sessionOptions } from "lib/session"; 5 | 6 | export const middleware = async (req: NextRequest) => { 7 | const res = NextResponse.next(); 8 | const session = await getIronSession(req, res, sessionOptions); 9 | 10 | // do anything with session here: 11 | const { user } = session; 12 | 13 | // like mutate user: 14 | // user.something = someOtherThing; 15 | // or: 16 | // session.user = someoneElse; 17 | 18 | // uncomment next line to commit changes: 19 | // await session.save(); 20 | // or maybe you want to destroy session: 21 | // await session.destroy(); 22 | 23 | console.log("from middleware", { user }); 24 | 25 | // demo: 26 | if (user?.login !== "vvo") { 27 | return new NextResponse(null, { status: 403 }); // unauthorized to see pages inside admin/ 28 | } 29 | 30 | return res; 31 | }; 32 | 33 | export const config = { 34 | matcher: "/admin", 35 | }; 36 | -------------------------------------------------------------------------------- /components/Form.tsx: -------------------------------------------------------------------------------- 1 | import { FormEvent } from "react"; 2 | 3 | export default function Form({ 4 | errorMessage, 5 | onSubmit, 6 | }: { 7 | errorMessage: string; 8 | onSubmit: (e: FormEvent) => void; 9 | }) { 10 | return ( 11 |
12 | 16 | 17 | 18 | 19 | {errorMessage &&

{errorMessage}

} 20 | 21 | 41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import Header from "components/Header"; 3 | 4 | export default function Layout({ children }: { children: React.ReactNode }) { 5 | return ( 6 | <> 7 | 8 | With Iron Session 9 | 10 | 32 |
33 | 34 |
35 |
{children}
36 |
37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Kyungeun Park 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.ko.md: -------------------------------------------------------------------------------- 1 | # trpc-iron-session 2 | 3 | [English](./README.md) | [`한국어(KR)`](./README.ko.md) 4 | 5 | `iron-session` 인증을 기반으로 `tRPC` 를 보호합니다. 6 | 7 | [iron-session examples next.js-typescript][project-structure-based] 프로젝트를 기반으로 구축되었습니다. 8 | 9 | 프로젝트 구조를 최대한 바꾸지 않고 `HTTP API` 부분을 `tRPC` 로 변경했습니다. 10 | 11 | 덕분에 해당 리포에 [iron-session examples next.js-typescript][project-structure-based]를 그대로 넣으면 `diff` 가 되어 변경 사항을 쉽게 확인할 수 있습니다. 12 | 13 | > 예제의 `SSG` 페이지는 `tRPC` 의 `SSR` 옵션 때문에 의미가 없어졌습니다. 🙄 14 | 15 | ## Start 16 | 17 | ```shell 18 | npm i 19 | npm run dev 20 | ``` 21 | 22 | ## Core 23 | 24 | ### SSR에 Cookies 전달하기 25 | 26 | `SSR` 을 통해서 로그인한 사용자의 경우 `
` 부분에 사용자 정보를 빠르게 표시하고 싶을 수 있습니다. 27 | 28 | 이렇게 하려면 `SSR` 동안 `tRPC` 에게 쿠키를 전달해야 합니다. 29 | 30 | 관련 코드는 [src/utils/trpc.ts](./src/utils/trpc.ts) 를 참고해주세요. 31 | 32 | ### `tRPC` Context에 `iron-session` 값 전달 33 | 34 | `iron-session` 에 익숙하다면 알겠지만 `req.session` 을 사용하여 쿠키를 조작합니다. 35 | 36 | 이를 위해서 `tRPC` 는 `Context` 로 값을 전달해야 합니다. 37 | 38 | 관련 코드는 [src/server/context.ts](./src/server/context.ts) 를 참고해주세요. 39 | 40 | ## Reference 41 | 42 | [해당 프로젝트를 기반으로 스케폴드 진행][project-structure-based] 43 | 44 | [project-structure-based]: https://github.com/vvo/iron-session/blob/main/examples/next.js-typescript/README.md 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next.js-typescript", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "@tanstack/react-query": "^4.14.5", 12 | "@trpc/client": "^10.0.0-rc.6", 13 | "@trpc/next": "^10.0.0-rc.6", 14 | "@trpc/react-query": "^10.0.0-rc.6", 15 | "@trpc/server": "^10.0.0-rc.6", 16 | "iron-session": "^6.2.0", 17 | "next": "^12.2.5", 18 | "octokit": "^2.0.7", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "zod": "^3.19.1" 22 | }, 23 | "devDependencies": { 24 | "@octokit/types": "^7.0.0", 25 | "@tanstack/react-query-devtools": "^4.14.5", 26 | "@types/react": "^18.0.17", 27 | "eslint-config-next": "latest", 28 | "typescript": "^4.7.4" 29 | }, 30 | "renovate": { 31 | "extends": [ 32 | "config:js-app", 33 | ":automergePatch", 34 | ":automergeBranch", 35 | ":automergePatch", 36 | ":automergeBranch", 37 | ":automergeLinters", 38 | ":automergeTesters", 39 | ":automergeTypes" 40 | ], 41 | "timezone": "Europe/Paris", 42 | "schedule": [ 43 | "before 3am on Monday" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # trpc-iron-session 2 | 3 | [`English`](./README.md) | [한국어(KR)](./README.ko.md) 4 | 5 | Secure your tRPC based on `iron-session` authentication. 6 | 7 | The project structure is based on [iron-session examples next.js-typescript][project-structure-based] 8 | 9 | Changed the `HTTP API` part to `tRPC` without changing the project structure as much as possible. 10 | 11 | Thanks to this, if you pour [iron-session examples next.js-typescript][project-structure-based] here as it is, it will `diff` so you can easily see the changes. 12 | 13 | > that the `SSG` page in the example is meaningless because of the `SSR` option of `tRPC`. 🙄 14 | 15 | ## Start 16 | 17 | ```shell 18 | npm i 19 | npm run dev 20 | ``` 21 | 22 | ## Core 23 | 24 | ### Forwarding SSR Cookies 25 | 26 | If you are logged in through `SSR` , you may want to quickly display user information in the `
` . 27 | 28 | To do this, you need to pass cookies to tRPC during SSR. 29 | 30 | Please refer to [src/utils/trpc.ts](./src/utils/trpc.ts) for the related code. 31 | 32 | ### Passing iron-session value to tRPC Context 33 | 34 | If you are familiar with `iron-session`, we will use the `req.session` to manipulate cookies. 35 | 36 | `tRPC` must be passed as a `Context` to achieve this. 37 | 38 | Please refer to [src/server/context.ts](./src/server/context.ts) for the related code. 39 | 40 | ## Reference 41 | 42 | [Projects used for scaffolding][project-structure-based] 43 | 44 | [project-structure-based]: https://github.com/vvo/iron-session/blob/main/examples/next.js-typescript/README.md 45 | -------------------------------------------------------------------------------- /pages/login.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import Layout from "components/Layout"; 3 | import Form from "components/Form"; 4 | import { trpc } from "src/utils/trpc"; 5 | import { useRouter } from "next/router"; 6 | import { withIronSessionSsr } from "iron-session/next"; 7 | import { sessionOptions } from "lib/session"; 8 | 9 | export default function Login() { 10 | const router = useRouter(); 11 | 12 | const login = trpc.session.login.useMutation({ 13 | onSuccess() { 14 | router.push("/profile-sg"); 15 | }, 16 | onError(err) { 17 | setErrorMsg(err.message); 18 | }, 19 | }); 20 | 21 | const [errorMsg, setErrorMsg] = useState(""); 22 | 23 | return ( 24 | 25 |
26 |
38 |
39 | 48 |
49 | ); 50 | } 51 | 52 | export const getServerSideProps = withIronSessionSsr(async function ({ 53 | req, 54 | res, 55 | }) { 56 | const user = req.session.user; 57 | 58 | if (user) { 59 | return { 60 | redirect: { 61 | destination: "/profile-sg", 62 | permanent: false, 63 | }, 64 | }; 65 | } 66 | 67 | return { 68 | props: {}, 69 | }; 70 | }, 71 | sessionOptions); 72 | -------------------------------------------------------------------------------- /pages/profile-ssr.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Layout from "components/Layout"; 3 | import { withIronSessionSsr } from "iron-session/next"; 4 | import { sessionOptions } from "lib/session"; 5 | import { User } from "lib/session"; 6 | 7 | import { InferGetServerSidePropsType } from "next"; 8 | 9 | export default function SsrProfile({ 10 | user, 11 | }: InferGetServerSidePropsType) { 12 | return ( 13 | 14 |

Your GitHub profile

15 |

16 | This page uses{" "} 17 | 18 | Server-side Rendering (SSR) 19 | {" "} 20 | and{" "} 21 | 22 | getServerSideProps 23 | 24 |

25 | 26 | {user?.isLoggedIn && ( 27 | <> 28 |

29 | Public data, from{" "} 30 | 31 | https://github.com/{user.login} 32 | 33 | , reduced to `login` and `avatar_url`. 34 |

35 |
{JSON.stringify(user, null, 2)}
36 | 37 | )} 38 |
39 | ); 40 | } 41 | 42 | export const getServerSideProps = withIronSessionSsr(async function ({ 43 | req, 44 | res, 45 | }) { 46 | const user = req.session.user; 47 | 48 | if (user === undefined) { 49 | res.setHeader("location", "/login"); 50 | res.statusCode = 302; 51 | res.end(); 52 | return { 53 | props: { 54 | user: { isLoggedIn: false, login: "", avatarUrl: "" } as User, 55 | }, 56 | }; 57 | } 58 | 59 | return { 60 | props: { user: req.session.user }, 61 | }; 62 | }, 63 | sessionOptions); 64 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Layout from "components/Layout"; 2 | import Image from "next/image"; 3 | 4 | export default function Home() { 5 | return ( 6 | 7 |

8 | 9 | 10 | 11 | iron-session - 12 | Authentication example 13 |

14 | 15 |

16 | This example creates an authentication system that uses a{" "} 17 | signed and encrypted cookie to store session data. 18 |

19 | 20 |

21 | It uses current best practices as for authentication in the Next.js 22 | ecosystem: 23 |
24 | 1. no `getInitialProps` to ensure every page is static 25 |
26 | 2. `useUser` hook together with ` 27 | swr` for data fetching 28 |

29 | 30 |

Features

31 | 32 |
    33 |
  • Logged in status synchronized between browser windows/tabs
  • 34 |
  • Layout based on logged in status
  • 35 |
  • All pages are static
  • 36 |
  • Session data is signed and encrypted in a cookie
  • 37 |
38 | 39 |

Steps to test the functionality:

40 | 41 |
    42 |
  1. Click login and enter your GitHub username.
  2. 43 |
  3. 44 | Click home and click profile again, notice how your session is being 45 | used through a token stored in a cookie. 46 |
  4. 47 |
  5. 48 | Click logout and try to go to profile again. You'll get 49 | redirected to the `/login` route. 50 |
  6. 51 |
52 | 57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /src/server/routers/session.ts: -------------------------------------------------------------------------------- 1 | import { router, publicProcedure } from "src/server/trpc"; 2 | import { z } from "zod"; 3 | import { User } from "lib/session"; 4 | import { Octokit } from "octokit"; 5 | import type { Endpoints } from "@octokit/types"; 6 | import { TRPCError } from "@trpc/server"; 7 | 8 | const octokit = new Octokit(); 9 | export type Events = 10 | Endpoints["GET /users/{username}/events"]["response"]["data"]; 11 | 12 | export const sessionRouter = router({ 13 | user: publicProcedure.query(async ({ ctx }) => { 14 | if (ctx.session.user) { 15 | return { 16 | ...ctx.session.user, 17 | isLoggedIn: true, 18 | }; 19 | } else { 20 | return { 21 | isLoggedIn: false, 22 | login: "", 23 | avatarUrl: "", 24 | }; 25 | } 26 | }), 27 | event: publicProcedure.query(async ({ ctx }) => { 28 | const user = ctx.session.user; 29 | 30 | if (!user || user.isLoggedIn === false) { 31 | throw new TRPCError({ 32 | code: "UNAUTHORIZED", 33 | }); 34 | } 35 | 36 | try { 37 | const { data: events } = 38 | await octokit.rest.activity.listPublicEventsForUser({ 39 | username: user.login, 40 | }); 41 | 42 | return events; 43 | } catch (error) { 44 | return []; 45 | } 46 | }), 47 | login: publicProcedure 48 | .input( 49 | z.object({ 50 | username: z.string(), 51 | }) 52 | ) 53 | .mutation(async ({ ctx, input }) => { 54 | const { username } = input; 55 | 56 | try { 57 | const { 58 | data: { login, avatar_url }, 59 | } = await octokit.rest.users.getByUsername({ username }); 60 | 61 | const user = { isLoggedIn: true, login, avatarUrl: avatar_url } as User; 62 | ctx.session.user = user; 63 | await ctx.session.save(); 64 | return user; 65 | } catch (error) { 66 | throw new TRPCError({ 67 | code: "INTERNAL_SERVER_ERROR", 68 | message: (error as Error).message, 69 | }); 70 | } 71 | }), 72 | logout: publicProcedure.mutation(async ({ ctx }) => { 73 | ctx.session.destroy(); 74 | return { isLoggedIn: false, login: "", avatarUrl: "" }; 75 | }), 76 | }); 77 | -------------------------------------------------------------------------------- /src/utils/trpc.ts: -------------------------------------------------------------------------------- 1 | import { httpBatchLink, loggerLink } from "@trpc/client"; 2 | import { createTRPCNext } from "@trpc/next"; 3 | import { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; 4 | // ℹ️ Type-only import: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export 5 | import type { AppRouter } from "src/server/routers/_app"; 6 | 7 | function getBaseUrl() { 8 | // 브라우저는 상대 경로를 사용해야 합니다: 해당 코드가 있어야 Client Side에서 서빙되는 도메인으로 요청이 됩니다 9 | if (typeof window !== "undefined") { 10 | return ""; 11 | } 12 | 13 | return `http://localhost:${process.env.PORT ?? 3000}`; 14 | } 15 | 16 | export const trpc = createTRPCNext({ 17 | config({ ctx }) { 18 | return { 19 | links: [ 20 | // 개발 중인 콘솔에 예쁜 로그를 추가하고 프로덕션에서 오류를 기록합니다. 21 | loggerLink({ 22 | enabled: (opts) => 23 | process.env.NODE_ENV === "development" || 24 | (opts.direction === "down" && opts.result instanceof Error), 25 | }), 26 | httpBatchLink({ 27 | url: `${getBaseUrl()}/api/trpc`, 28 | headers() { 29 | if (ctx?.req) { 30 | /** 31 | * SSR을 제대로 사용하려면 클라이언트의 헤더를 서버로 전달해야 합니다. 32 | * 이는 SSR시 쿠키와 같은 것을 전달할 수 있도록 하기 위한 것입니다. 33 | * 해당 작업을 진행하지 않으면 SSR시 쿠키가 전달되지 않음으로 SSR에서 쿠키 값으로 사용자 정보를 SSR 굽는 등 iron-session 처리를 할 수 없습니다. 34 | * 햇갈리면 안되는 것은 해당 작업은 SSR시 처리를 위함이며 해당 코드가 없다고 해서 Client Side에서 호출되는 tRPC가 iron-session 처리를 못한다는 것이 아닙니다. 35 | * @see [가져온 코드 Origin](https://trpc.io/docs/v10/ssr) 36 | */ 37 | const { connection: _connection, ...headers } = ctx.req.headers; 38 | return { 39 | ...headers, 40 | }; 41 | } 42 | return {}; 43 | }, 44 | }), 45 | ], 46 | queryClientConfig: { 47 | defaultOptions: { 48 | queries: { 49 | // react-query 옵션 설정 방법 예제 추가하는 겸 재시도 계속하면 디버깅 힘들어서 설정 50 | retry: 1, 51 | }, 52 | }, 53 | }, 54 | }; 55 | }, 56 | ssr: true, 57 | }); 58 | 59 | export type RouterInput = inferRouterInputs; 60 | export type RouterOutput = inferRouterOutputs; 61 | -------------------------------------------------------------------------------- /pages/profile-sg.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Layout from "components/Layout"; 3 | import { trpc } from "src/utils/trpc"; 4 | import { withIronSessionSsr } from "iron-session/next"; 5 | import { sessionOptions } from "lib/session"; 6 | import { useRouter } from "next/router"; 7 | 8 | // Make sure to check https://nextjs.org/docs/basic-features/layouts for more info on how to use layouts 9 | export default function SgProfile() { 10 | const router = useRouter(); 11 | 12 | const userQuery = trpc.session.user.useQuery(undefined, { 13 | /** 14 | * NOTE: 꼭 사용자 정보 쿼리가 사용자 정보가 없는 경우 로그인 페이지로 리디렉션 시켜야하는 것은 아닙니다. 15 | * `
` 와 같이 모든 페이지에서 호출되긴 하는데 정보가 없으면 없는데로 사용하는 경우가 있기 때문입니다. 16 | */ 17 | onSuccess(data) { 18 | if (data.isLoggedIn === false) { 19 | router.push("/login"); 20 | } 21 | }, 22 | }); 23 | const eventQuery = trpc.session.event.useQuery(undefined, { 24 | // 사용자가 로그인한 경우에만 수행합니다. 25 | enabled: userQuery.data?.isLoggedIn, 26 | }); 27 | 28 | const user = userQuery.data; 29 | const events = eventQuery.data; 30 | 31 | return ( 32 | 33 |

Your GitHub profile

34 |

35 | This page uses{" "} 36 | 37 | Static Generation (SG) 38 | {" "} 39 | {/* eslint-disable-next-line @next/next/no-html-link-for-pages */} 40 | and the /api/user route (using{" "} 41 | vercel/SWR) 42 |

43 | {user && ( 44 | <> 45 |

46 | Public data, from{" "} 47 | 48 | https://github.com/{user.login} 49 | 50 | , reduced to `login` and `avatar_url`. 51 |

52 | 53 |
{JSON.stringify(user, null, 2)}
54 | 55 | )} 56 | 57 | {events !== undefined && ( 58 |

59 | Number of GitHub events for user: {events.length}.{" "} 60 | {events.length > 0 && ( 61 | <> 62 | Last event type: {events[0].type} 63 | 64 | )} 65 |

66 | )} 67 |
68 | ); 69 | } 70 | 71 | export const getServerSideProps = withIronSessionSsr(async function ({ 72 | req, 73 | res, 74 | }) { 75 | const user = req.session.user; 76 | 77 | if (!user) { 78 | return { 79 | redirect: { 80 | destination: "/login", 81 | permanent: false, 82 | }, 83 | }; 84 | } 85 | 86 | return { 87 | props: {}, 88 | }; 89 | }, 90 | sessionOptions); 91 | -------------------------------------------------------------------------------- /components/Header.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { useRouter } from "next/router"; 3 | import Image from "next/image"; 4 | import { trpc } from "src/utils/trpc"; 5 | 6 | export default function Header() { 7 | const router = useRouter(); 8 | 9 | const userQuery = trpc.session.user.useQuery(); 10 | const logout = trpc.session.logout.useMutation({ 11 | onSuccess() { 12 | router.push("/login"); 13 | }, 14 | }); 15 | 16 | const user = userQuery.data; 17 | 18 | return ( 19 |
20 | 90 | 124 |
125 | ); 126 | } 127 | --------------------------------------------------------------------------------