├── .gitignore
├── .vscode
└── settings.json
├── csr-basic-auth
├── eslint.config.js
├── index.html
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── scripts
│ ├── build.ts
│ ├── dev.ts
│ └── start.ts
├── src
│ ├── api
│ │ ├── auth-mutation.ts
│ │ └── auth-query.ts
│ ├── app.tsx
│ ├── components
│ │ └── navigation.tsx
│ ├── hooks
│ │ └── use-auth.ts
│ ├── lib
│ │ ├── constants.ts
│ │ ├── ky-with-auth.ts
│ │ ├── query.ts
│ │ ├── router.tsx
│ │ └── schema.ts
│ ├── main.tsx
│ ├── route-tree.gen.ts
│ ├── routes
│ │ ├── __root.tsx
│ │ ├── _auth.sign-in.tsx
│ │ ├── _auth.tsx
│ │ ├── _authenticated.secret.tsx
│ │ ├── _authenticated.tsx
│ │ └── index.tsx
│ ├── server.ts
│ ├── styles
│ │ └── global.css
│ └── types
│ │ └── response.ts
├── tailwind.config.ts
├── tsconfig.json
└── vite.config.ts
└── csr-jwt
├── eslint.config.js
├── index.html
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── scripts
├── build.ts
├── dev.ts
└── start.ts
├── src
├── api
│ ├── auth-query.ts
│ ├── sign-in-mutation.ts
│ └── sign-out-mutation.ts
├── app.tsx
├── components
│ └── navigation.tsx
├── hooks
│ └── use-auth.ts
├── lib
│ ├── constants.ts
│ ├── ky-with-auth.ts
│ ├── query.ts
│ ├── router.tsx
│ └── schema.ts
├── main.tsx
├── route-tree.gen.ts
├── routes
│ ├── __root.tsx
│ ├── _auth.sign-in.tsx
│ ├── _auth.tsx
│ ├── _authenticated.secret.tsx
│ ├── _authenticated.tsx
│ └── index.tsx
├── server.ts
├── styles
│ └── global.css
└── types
│ └── response.ts
├── tailwind.config.ts
├── tsconfig.json
└── vite.config.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | # Local
2 | .DS_Store
3 | *.local
4 | *.log*
5 |
6 | # Dist
7 | node_modules
8 | dist/
9 | .vinxi
10 | .output
11 | .vercel
12 | .netlify
13 | .wrangler
14 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": false,
3 | "editor.codeActionsOnSave": {
4 | "source.fixAll.eslint": "always"
5 | },
6 | }
--------------------------------------------------------------------------------
/csr-basic-auth/eslint.config.js:
--------------------------------------------------------------------------------
1 | import nekoConfig from '@nekochan0122/config/eslint'
2 | import eslintPluginQuery from '@tanstack/eslint-plugin-query'
3 | import eslintPluginTailwind from 'eslint-plugin-tailwindcss'
4 | import globals from 'globals'
5 | import tseslint from 'typescript-eslint'
6 |
7 | export default tseslint.config(
8 | ...nekoConfig.presets.react,
9 | ...eslintPluginQuery.configs['flat/recommended'],
10 | ...eslintPluginTailwind.configs['flat/recommended'],
11 | {
12 | languageOptions: {
13 | globals: globals.browser,
14 | },
15 | },
16 | {
17 | ignores: [
18 | 'dist',
19 | './src/route-tree.gen.ts',
20 | ],
21 | },
22 | )
23 |
--------------------------------------------------------------------------------
/csr-basic-auth/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | csr-basic-auth
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/csr-basic-auth/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "csr-basic-auth",
3 | "type": "module",
4 | "scripts": {
5 | "dev": "tsx scripts/dev.ts",
6 | "build": "tsx scripts/build.ts",
7 | "start": "tsx scripts/start.ts",
8 | "client:dev": "vite",
9 | "client:build": "vite build",
10 | "client:serve": "vite preview",
11 | "client:start": "vite",
12 | "server:dev": "tsx watch src/server.ts",
13 | "server:start": "tsx src/server.ts",
14 | "typecheck": "tsc --noEmit",
15 | "lint": "eslint .",
16 | "lint:fix": "eslint . --fix"
17 | },
18 | "dependencies": {
19 | "@hono/node-server": "^1.12.2",
20 | "@tanstack/react-query": "^5.56.2",
21 | "@tanstack/react-query-devtools": "^5.56.2",
22 | "@tanstack/react-router": "^1.57.13",
23 | "@tanstack/router-devtools": "^1.57.13",
24 | "hono": "^4.6.1",
25 | "ky": "^1.7.2",
26 | "react": "^18.3.1",
27 | "react-dom": "^18.3.1",
28 | "zod": "^3.23.8"
29 | },
30 | "devDependencies": {
31 | "@eslint/compat": "^1.1.1",
32 | "@eslint/js": "^9.10.0",
33 | "@nekochan0122/config": "^1.8.0",
34 | "@stylistic/eslint-plugin": "^2.8.0",
35 | "@tanstack/eslint-plugin-query": "^5.56.1",
36 | "@tanstack/router-plugin": "^1.57.13",
37 | "@types/eslint__js": "^8.42.3",
38 | "@types/node": "^22.5.5",
39 | "@types/react": "^18.3.5",
40 | "@types/react-dom": "^18.3.0",
41 | "@vitejs/plugin-react-swc": "^3.7.0",
42 | "autoprefixer": "^10.4.20",
43 | "concurrently": "^9.0.1",
44 | "eslint": "^9.10.0",
45 | "eslint-plugin-import": "^2.30.0",
46 | "eslint-plugin-jsx-a11y": "^6.10.0",
47 | "eslint-plugin-react": "^7.36.1",
48 | "eslint-plugin-react-hooks": "^4.6.2",
49 | "eslint-plugin-react-refresh": "^0.4.12",
50 | "eslint-plugin-simple-import-sort": "^12.1.1",
51 | "eslint-plugin-tailwindcss": "^3.17.4",
52 | "eslint-plugin-unicorn": "^55.0.0",
53 | "globals": "^15.9.0",
54 | "postcss": "^8.4.45",
55 | "tailwindcss": "^3.4.11",
56 | "tsx": "^4.19.1",
57 | "typescript": "^5.6.2",
58 | "typescript-eslint": "^8.5.0",
59 | "vite": "^5.4.5",
60 | "vite-tsconfig-paths": "^5.0.1"
61 | }
62 | }
--------------------------------------------------------------------------------
/csr-basic-auth/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/csr-basic-auth/scripts/build.ts:
--------------------------------------------------------------------------------
1 | import concurrently from 'concurrently'
2 |
3 | concurrently(
4 | [
5 | {
6 | name: 'client',
7 | prefixColor: '#58c4dc',
8 | command: 'pnpm client:build',
9 | },
10 | ],
11 | )
12 |
--------------------------------------------------------------------------------
/csr-basic-auth/scripts/dev.ts:
--------------------------------------------------------------------------------
1 | import concurrently from 'concurrently'
2 |
3 | concurrently(
4 | [
5 | {
6 | name: 'client',
7 | prefixColor: '#58c4dc',
8 | command: 'pnpm client:dev',
9 | },
10 | {
11 | name: 'server',
12 | prefixColor: '#fe6f32',
13 | command: 'pnpm server:dev',
14 | },
15 | ],
16 | )
17 |
--------------------------------------------------------------------------------
/csr-basic-auth/scripts/start.ts:
--------------------------------------------------------------------------------
1 | import concurrently from 'concurrently'
2 |
3 | concurrently(
4 | [
5 | {
6 | name: 'client',
7 | prefixColor: '#58c4dc',
8 | command: 'pnpm client:start',
9 | },
10 | {
11 | name: 'server',
12 | prefixColor: '#fe6f32',
13 | command: 'pnpm server:start',
14 | },
15 | ],
16 | )
17 |
--------------------------------------------------------------------------------
/csr-basic-auth/src/api/auth-mutation.ts:
--------------------------------------------------------------------------------
1 | import { useMutation, useQueryClient } from '@tanstack/react-query'
2 |
3 | import { ENCODED_CREDENTIALS } from '~/lib/constants'
4 | import { ky } from '~/lib/ky-with-auth'
5 | import type { SignInFormSchema } from '~/lib/schema'
6 | import type { ResAuthUser } from '~/types/response'
7 |
8 | export function useAuthMutation() {
9 | const queryClient = useQueryClient()
10 |
11 | return useMutation({
12 | mutationKey: ['auth-mutation-for-sign-in'],
13 | mutationFn: async ({ username, password }: SignInFormSchema) => {
14 | const encodedCredentials = btoa(`${username}:${password}`)
15 |
16 | sessionStorage.setItem(ENCODED_CREDENTIALS, encodedCredentials)
17 |
18 | return ky.get('auth').json()
19 | },
20 | onSuccess(data) {
21 | queryClient.setQueryData(['auth'], data)
22 | },
23 | onError() {
24 | queryClient.setQueryData(['auth'], null)
25 | sessionStorage.removeItem(ENCODED_CREDENTIALS)
26 | },
27 | retry: false,
28 | })
29 | }
30 |
--------------------------------------------------------------------------------
/csr-basic-auth/src/api/auth-query.ts:
--------------------------------------------------------------------------------
1 | import { queryOptions, useQuery } from '@tanstack/react-query'
2 |
3 | import { ky } from '~/lib/ky-with-auth'
4 | import type { ResAuthUser } from '~/types/response'
5 |
6 | export function authOptions() {
7 | return queryOptions({
8 | queryKey: ['auth'],
9 | queryFn: () => ky.get('auth').json(),
10 | retry: false,
11 | })
12 | }
13 |
14 | export function useAuthQuery() {
15 | return useQuery(authOptions())
16 | }
17 |
--------------------------------------------------------------------------------
/csr-basic-auth/src/app.tsx:
--------------------------------------------------------------------------------
1 | import { QueryClientProvider } from '@tanstack/react-query'
2 | import { RouterProvider } from '@tanstack/react-router'
3 |
4 | import { useAuth } from '~/hooks/use-auth'
5 | import { queryClient } from '~/lib/query'
6 | import { router } from '~/lib/router'
7 |
8 | function App() {
9 | return (
10 |
11 |
12 |
13 | )
14 | }
15 |
16 | function RouterProviderWithContext() {
17 | const auth = useAuth()
18 |
19 | return
20 | }
21 |
22 | export { App }
23 |
--------------------------------------------------------------------------------
/csr-basic-auth/src/components/navigation.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from '@tanstack/react-router'
2 |
3 | import { useAuth } from '~/hooks/use-auth'
4 | import type { FileRouteTypes } from '~/route-tree.gen'
5 |
6 | type NavigationItem = {
7 | to: FileRouteTypes['to']
8 | name: string
9 | }
10 |
11 | const navigation: NavigationItem[] = [
12 | {
13 | to: '/',
14 | name: 'Home',
15 | },
16 | {
17 | to: '/secret',
18 | name: 'Secret',
19 | },
20 | ]
21 |
22 | export function Navigation() {
23 | const auth = useAuth()
24 |
25 | return (
26 |
58 | )
59 | }
60 |
--------------------------------------------------------------------------------
/csr-basic-auth/src/hooks/use-auth.ts:
--------------------------------------------------------------------------------
1 | import { useQueryClient } from '@tanstack/react-query'
2 | import { useEffect } from 'react'
3 |
4 | import { authOptions, useAuthQuery } from '~/api/auth-query'
5 | import { ENCODED_CREDENTIALS } from '~/lib/constants'
6 | import { router } from '~/lib/router'
7 | import type { ResAuthUser } from '~/types/response'
8 |
9 | type AuthState =
10 | | { user: null; status: 'PENDING' }
11 | | { user: null; status: 'UNAUTHENTICATED' }
12 | | { user: ResAuthUser; status: 'AUTHENTICATED' }
13 |
14 | type AuthUtils = {
15 | signIn: () => void
16 | signOut: () => void
17 | ensureData: () => Promise
18 | }
19 |
20 | type AuthData = AuthState & AuthUtils
21 |
22 | function useAuth(): AuthData {
23 | const userQuery = useAuthQuery()
24 | const queryClient = useQueryClient()
25 |
26 | useEffect(() => {
27 | router.invalidate()
28 | }, [userQuery.data])
29 |
30 | const utils: AuthUtils = {
31 | signIn: () => {
32 | router.navigate({ to: '/sign-in' })
33 | },
34 | signOut: () => {
35 | sessionStorage.removeItem(ENCODED_CREDENTIALS)
36 | queryClient.setQueryData(['auth'], null)
37 | },
38 | ensureData: () => {
39 | return queryClient.ensureQueryData(
40 | authOptions(),
41 | )
42 | },
43 | }
44 |
45 | switch (true) {
46 | case userQuery.isPending:
47 | return { ...utils, user: null, status: 'PENDING' }
48 |
49 | case !userQuery.data:
50 | return { ...utils, user: null, status: 'UNAUTHENTICATED' }
51 |
52 | default:
53 | return { ...utils, user: userQuery.data, status: 'AUTHENTICATED' }
54 | }
55 | }
56 |
57 | export { useAuth }
58 | export type { AuthData }
59 |
--------------------------------------------------------------------------------
/csr-basic-auth/src/lib/constants.ts:
--------------------------------------------------------------------------------
1 | export const ENCODED_CREDENTIALS = 'encoded-credentials'
2 |
--------------------------------------------------------------------------------
/csr-basic-auth/src/lib/ky-with-auth.ts:
--------------------------------------------------------------------------------
1 | import ky from 'ky'
2 |
3 | import { ENCODED_CREDENTIALS } from '~/lib/constants'
4 |
5 | const kyWithAuth = ky.create({
6 | prefixUrl: 'http://localhost:6969',
7 | hooks: {
8 | beforeRequest: [
9 | (request) => {
10 | const encodedCredentials = sessionStorage.getItem(ENCODED_CREDENTIALS)
11 | request.headers.set('Authorization', `Basic ${encodedCredentials}`)
12 | },
13 | ],
14 | },
15 | })
16 |
17 | export { kyWithAuth as ky }
18 |
--------------------------------------------------------------------------------
/csr-basic-auth/src/lib/query.ts:
--------------------------------------------------------------------------------
1 | import { QueryClient } from '@tanstack/react-query'
2 |
3 | export const queryClient = new QueryClient()
4 |
--------------------------------------------------------------------------------
/csr-basic-auth/src/lib/router.tsx:
--------------------------------------------------------------------------------
1 | import { createRouter } from '@tanstack/react-router'
2 | import type { QueryClient } from '@tanstack/react-query'
3 |
4 | import { queryClient } from '~/lib/query'
5 | import { routeTree } from '~/route-tree.gen'
6 | import type { AuthData } from '~/hooks/use-auth'
7 |
8 | type RouterContext = {
9 | auth: AuthData
10 | queryClient: QueryClient
11 | }
12 |
13 | const router = createRouter({
14 | routeTree,
15 | defaultPreload: 'intent',
16 | context: {
17 | auth: null as unknown as AuthData,
18 | queryClient,
19 | },
20 | })
21 |
22 | declare module '@tanstack/react-router' {
23 | interface Register {
24 | router: typeof router
25 | }
26 | }
27 |
28 | export { router }
29 | export type { RouterContext }
30 |
--------------------------------------------------------------------------------
/csr-basic-auth/src/lib/schema.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 |
3 | export const signInFormSchema = z.object({
4 | username: z.string(),
5 | password: z.string(),
6 | })
7 |
8 | export type SignInFormSchema = z.infer
9 |
--------------------------------------------------------------------------------
/csr-basic-auth/src/main.tsx:
--------------------------------------------------------------------------------
1 | import '~/styles/global.css'
2 |
3 | import { createRoot } from 'react-dom/client'
4 |
5 | import { App } from '~/app'
6 |
7 | declare global {
8 | interface Window {
9 | readonly app: HTMLElement
10 | }
11 | }
12 |
13 | createRoot(window.app).render()
14 |
--------------------------------------------------------------------------------
/csr-basic-auth/src/route-tree.gen.ts:
--------------------------------------------------------------------------------
1 | /* prettier-ignore-start */
2 |
3 | /* eslint-disable */
4 |
5 | // @ts-nocheck
6 |
7 | // noinspection JSUnusedGlobalSymbols
8 |
9 | // This file is auto-generated by TanStack Router
10 |
11 | // Import Routes
12 |
13 | import { Route as rootRoute } from './routes/__root'
14 | import { Route as AuthenticatedImport } from './routes/_authenticated'
15 | import { Route as AuthImport } from './routes/_auth'
16 | import { Route as IndexImport } from './routes/index'
17 | import { Route as AuthenticatedSecretImport } from './routes/_authenticated.secret'
18 | import { Route as AuthSignInImport } from './routes/_auth.sign-in'
19 |
20 | // Create/Update Routes
21 |
22 | const AuthenticatedRoute = AuthenticatedImport.update({
23 | id: '/_authenticated',
24 | getParentRoute: () => rootRoute,
25 | } as any)
26 |
27 | const AuthRoute = AuthImport.update({
28 | id: '/_auth',
29 | getParentRoute: () => rootRoute,
30 | } as any)
31 |
32 | const IndexRoute = IndexImport.update({
33 | path: '/',
34 | getParentRoute: () => rootRoute,
35 | } as any)
36 |
37 | const AuthenticatedSecretRoute = AuthenticatedSecretImport.update({
38 | path: '/secret',
39 | getParentRoute: () => AuthenticatedRoute,
40 | } as any)
41 |
42 | const AuthSignInRoute = AuthSignInImport.update({
43 | path: '/sign-in',
44 | getParentRoute: () => AuthRoute,
45 | } as any)
46 |
47 | // Populate the FileRoutesByPath interface
48 |
49 | declare module '@tanstack/react-router' {
50 | interface FileRoutesByPath {
51 | '/': {
52 | id: '/'
53 | path: '/'
54 | fullPath: '/'
55 | preLoaderRoute: typeof IndexImport
56 | parentRoute: typeof rootRoute
57 | }
58 | '/_auth': {
59 | id: '/_auth'
60 | path: ''
61 | fullPath: ''
62 | preLoaderRoute: typeof AuthImport
63 | parentRoute: typeof rootRoute
64 | }
65 | '/_authenticated': {
66 | id: '/_authenticated'
67 | path: ''
68 | fullPath: ''
69 | preLoaderRoute: typeof AuthenticatedImport
70 | parentRoute: typeof rootRoute
71 | }
72 | '/_auth/sign-in': {
73 | id: '/_auth/sign-in'
74 | path: '/sign-in'
75 | fullPath: '/sign-in'
76 | preLoaderRoute: typeof AuthSignInImport
77 | parentRoute: typeof AuthImport
78 | }
79 | '/_authenticated/secret': {
80 | id: '/_authenticated/secret'
81 | path: '/secret'
82 | fullPath: '/secret'
83 | preLoaderRoute: typeof AuthenticatedSecretImport
84 | parentRoute: typeof AuthenticatedImport
85 | }
86 | }
87 | }
88 |
89 | // Create and export the route tree
90 |
91 | interface AuthRouteChildren {
92 | AuthSignInRoute: typeof AuthSignInRoute
93 | }
94 |
95 | const AuthRouteChildren: AuthRouteChildren = {
96 | AuthSignInRoute: AuthSignInRoute,
97 | }
98 |
99 | const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
100 |
101 | interface AuthenticatedRouteChildren {
102 | AuthenticatedSecretRoute: typeof AuthenticatedSecretRoute
103 | }
104 |
105 | const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
106 | AuthenticatedSecretRoute: AuthenticatedSecretRoute,
107 | }
108 |
109 | const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren(
110 | AuthenticatedRouteChildren,
111 | )
112 |
113 | export interface FileRoutesByFullPath {
114 | '/': typeof IndexRoute
115 | '': typeof AuthenticatedRouteWithChildren
116 | '/sign-in': typeof AuthSignInRoute
117 | '/secret': typeof AuthenticatedSecretRoute
118 | }
119 |
120 | export interface FileRoutesByTo {
121 | '/': typeof IndexRoute
122 | '': typeof AuthenticatedRouteWithChildren
123 | '/sign-in': typeof AuthSignInRoute
124 | '/secret': typeof AuthenticatedSecretRoute
125 | }
126 |
127 | export interface FileRoutesById {
128 | __root__: typeof rootRoute
129 | '/': typeof IndexRoute
130 | '/_auth': typeof AuthRouteWithChildren
131 | '/_authenticated': typeof AuthenticatedRouteWithChildren
132 | '/_auth/sign-in': typeof AuthSignInRoute
133 | '/_authenticated/secret': typeof AuthenticatedSecretRoute
134 | }
135 |
136 | export interface FileRouteTypes {
137 | fileRoutesByFullPath: FileRoutesByFullPath
138 | fullPaths: '/' | '' | '/sign-in' | '/secret'
139 | fileRoutesByTo: FileRoutesByTo
140 | to: '/' | '' | '/sign-in' | '/secret'
141 | id:
142 | | '__root__'
143 | | '/'
144 | | '/_auth'
145 | | '/_authenticated'
146 | | '/_auth/sign-in'
147 | | '/_authenticated/secret'
148 | fileRoutesById: FileRoutesById
149 | }
150 |
151 | export interface RootRouteChildren {
152 | IndexRoute: typeof IndexRoute
153 | AuthRoute: typeof AuthRouteWithChildren
154 | AuthenticatedRoute: typeof AuthenticatedRouteWithChildren
155 | }
156 |
157 | const rootRouteChildren: RootRouteChildren = {
158 | IndexRoute: IndexRoute,
159 | AuthRoute: AuthRouteWithChildren,
160 | AuthenticatedRoute: AuthenticatedRouteWithChildren,
161 | }
162 |
163 | export const routeTree = rootRoute
164 | ._addFileChildren(rootRouteChildren)
165 | ._addFileTypes()
166 |
167 | /* prettier-ignore-end */
168 |
169 | /* ROUTE_MANIFEST_START
170 | {
171 | "routes": {
172 | "__root__": {
173 | "filePath": "__root.tsx",
174 | "children": [
175 | "/",
176 | "/_auth",
177 | "/_authenticated"
178 | ]
179 | },
180 | "/": {
181 | "filePath": "index.tsx"
182 | },
183 | "/_auth": {
184 | "filePath": "_auth.tsx",
185 | "children": [
186 | "/_auth/sign-in"
187 | ]
188 | },
189 | "/_authenticated": {
190 | "filePath": "_authenticated.tsx",
191 | "children": [
192 | "/_authenticated/secret"
193 | ]
194 | },
195 | "/_auth/sign-in": {
196 | "filePath": "_auth.sign-in.tsx",
197 | "parent": "/_auth"
198 | },
199 | "/_authenticated/secret": {
200 | "filePath": "_authenticated.secret.tsx",
201 | "parent": "/_authenticated"
202 | }
203 | }
204 | }
205 | ROUTE_MANIFEST_END */
206 |
--------------------------------------------------------------------------------
/csr-basic-auth/src/routes/__root.tsx:
--------------------------------------------------------------------------------
1 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
2 | import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'
3 | import { TanStackRouterDevtools } from '@tanstack/router-devtools'
4 |
5 | import { Navigation } from '~/components/navigation'
6 | import type { RouterContext } from '~/lib/router'
7 |
8 | export const Route = createRootRouteWithContext()({
9 | component: RootComponent,
10 | })
11 |
12 | function RootComponent() {
13 | return (
14 | <>
15 |
16 |
17 |
18 |
19 |
20 |
21 | >
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/csr-basic-auth/src/routes/_auth.sign-in.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute } from '@tanstack/react-router'
2 | import type { FormEvent } from 'react'
3 |
4 | import { useAuthMutation } from '~/api/auth-mutation'
5 | import { signInFormSchema } from '~/lib/schema'
6 |
7 | export const Route = createFileRoute('/_auth/sign-in')({
8 | component: SignInComponent,
9 | })
10 |
11 | function SignInComponent() {
12 | const userMutation = useAuthMutation()
13 |
14 | async function handleSubmit(e: FormEvent) {
15 | e.preventDefault()
16 |
17 | const form = e.currentTarget
18 | const formData = new FormData(form)
19 | const data = Object.fromEntries(formData)
20 | const dataParsed = signInFormSchema.parse(data)
21 |
22 | await userMutation.mutateAsync(dataParsed)
23 | }
24 |
25 | return (
26 | <>
27 |
35 | {userMutation.error && {userMutation.error.message}
}
36 | >
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/csr-basic-auth/src/routes/_auth.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
2 | import { z } from 'zod'
3 |
4 | export const Route = createFileRoute('/_auth')({
5 | validateSearch: z.object({
6 | redirect: z.string().optional().catch(''),
7 | }),
8 | beforeLoad: ({ context, search }) => {
9 | if (context.auth.status === 'AUTHENTICATED') {
10 | throw redirect({
11 | to: search.redirect || '/',
12 | })
13 | }
14 | },
15 | component: AuthLayout,
16 | })
17 |
18 | function AuthLayout() {
19 | return (
20 | <>
21 |
22 | >
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/csr-basic-auth/src/routes/_authenticated.secret.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute } from '@tanstack/react-router'
2 |
3 | export const Route = createFileRoute('/_authenticated/secret')({
4 | component: SecretRoute,
5 | })
6 |
7 | function SecretRoute() {
8 | return (
9 | <>
10 | This is a secret page
11 | >
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/csr-basic-auth/src/routes/_authenticated.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
2 |
3 | export const Route = createFileRoute('/_authenticated')({
4 | beforeLoad: async ({ context, location }) => {
5 | let shouldRedirect = false
6 |
7 | if (context.auth.status === 'PENDING') {
8 | const data = await context.auth.ensureData()
9 |
10 | if (!data) {
11 | shouldRedirect = true
12 | }
13 | }
14 |
15 | if (context.auth.status === 'UNAUTHENTICATED') {
16 | shouldRedirect = true
17 | }
18 |
19 | if (shouldRedirect) {
20 | throw redirect({
21 | to: '/sign-in',
22 | search: {
23 | redirect: location.href,
24 | },
25 | })
26 | }
27 | },
28 | component: AuthenticatedLayout,
29 | })
30 |
31 | function AuthenticatedLayout() {
32 | return (
33 | <>
34 |
35 | >
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/csr-basic-auth/src/routes/index.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute } from '@tanstack/react-router'
2 |
3 | import { useAuth } from '~/hooks/use-auth'
4 |
5 | export const Route = createFileRoute('/')({
6 | component: HomeComponent,
7 | })
8 |
9 | function HomeComponent() {
10 | const auth = useAuth()
11 |
12 | return (
13 | <>
14 | Welcome {auth.user?.name || 'Guest'}!
15 | >
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/csr-basic-auth/src/server.ts:
--------------------------------------------------------------------------------
1 | import { serve } from '@hono/node-server'
2 | import { Hono } from 'hono'
3 | import { basicAuth } from 'hono/basic-auth'
4 | import { cors } from 'hono/cors'
5 |
6 | const app = new Hono()
7 | const port = 6969
8 |
9 | app.use('*', cors())
10 |
11 | app.get('/', (c) => {
12 | return c.text('Hello Hono!')
13 | })
14 |
15 | app.use(
16 | '/auth/*',
17 | basicAuth({
18 | username: 'admin',
19 | password: 'admin',
20 | }),
21 | )
22 |
23 | app.get('/auth', async (c) => {
24 | return c.json({
25 | name: 'John Doe',
26 | })
27 | })
28 |
29 | serve({
30 | fetch: app.fetch,
31 | port,
32 | })
33 |
34 | console.log(`Server is running at http://localhost:${port}`)
35 |
--------------------------------------------------------------------------------
/csr-basic-auth/src/styles/global.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/csr-basic-auth/src/types/response.ts:
--------------------------------------------------------------------------------
1 | export type ResAuthUser = {
2 | name: string
3 | }
4 |
--------------------------------------------------------------------------------
/csr-basic-auth/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss'
2 |
3 | export default {
4 | content: [
5 | './index.html',
6 | './src/**/*.tsx',
7 | ],
8 | theme: {
9 | extend: {},
10 | },
11 | plugins: [],
12 | } satisfies Config
13 |
--------------------------------------------------------------------------------
/csr-basic-auth/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "skipLibCheck": true,
5 | "esModuleInterop": true,
6 | "jsx": "react-jsx",
7 | "target": "ESNext",
8 | "module": "ESNext",
9 | "moduleResolution": "Bundler",
10 | "baseUrl": ".",
11 | "paths": {
12 | "~/*": ["./src/*"]
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/csr-basic-auth/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
2 | import react from '@vitejs/plugin-react-swc'
3 | import { defineConfig } from 'vite'
4 | import tsconfigPaths from 'vite-tsconfig-paths'
5 |
6 | export default defineConfig({
7 | plugins: [
8 | TanStackRouterVite({
9 | generatedRouteTree: 'src/route-tree.gen.ts',
10 | }),
11 | tsconfigPaths(),
12 | react(),
13 | ],
14 | })
15 |
--------------------------------------------------------------------------------
/csr-jwt/eslint.config.js:
--------------------------------------------------------------------------------
1 | import nekoConfig from '@nekochan0122/config/eslint'
2 | import eslintPluginQuery from '@tanstack/eslint-plugin-query'
3 | import eslintPluginTailwind from 'eslint-plugin-tailwindcss'
4 | import globals from 'globals'
5 | import tseslint from 'typescript-eslint'
6 |
7 | export default tseslint.config(
8 | ...nekoConfig.presets.react,
9 | ...eslintPluginQuery.configs['flat/recommended'],
10 | ...eslintPluginTailwind.configs['flat/recommended'],
11 | {
12 | languageOptions: {
13 | globals: globals.browser,
14 | },
15 | },
16 | {
17 | ignores: [
18 | 'dist',
19 | './src/route-tree.gen.ts',
20 | ],
21 | },
22 | )
23 |
--------------------------------------------------------------------------------
/csr-jwt/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | csr-jwt
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/csr-jwt/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "csr-jwt",
3 | "type": "module",
4 | "scripts": {
5 | "dev": "tsx scripts/dev.ts",
6 | "build": "tsx scripts/build.ts",
7 | "start": "tsx scripts/start.ts",
8 | "client:dev": "vite",
9 | "client:build": "vite build",
10 | "client:serve": "vite preview",
11 | "client:start": "vite",
12 | "server:dev": "tsx watch src/server.ts",
13 | "server:start": "tsx src/server.ts",
14 | "typecheck": "tsc --noEmit",
15 | "lint": "eslint .",
16 | "lint:fix": "eslint . --fix"
17 | },
18 | "dependencies": {
19 | "@hono/node-server": "^1.12.2",
20 | "@node-rs/argon2": "^1.8.3",
21 | "@tanstack/react-query": "^5.56.2",
22 | "@tanstack/react-query-devtools": "^5.56.2",
23 | "@tanstack/react-router": "^1.57.13",
24 | "@tanstack/router-devtools": "^1.57.13",
25 | "hono": "^4.6.1",
26 | "ky": "^1.7.2",
27 | "react": "^18.3.1",
28 | "react-dom": "^18.3.1",
29 | "zod": "^3.23.8"
30 | },
31 | "devDependencies": {
32 | "@eslint/compat": "^1.1.1",
33 | "@eslint/js": "^9.10.0",
34 | "@hono/zod-validator": "^0.2.2",
35 | "@nekochan0122/config": "^1.8.0",
36 | "@stylistic/eslint-plugin": "^2.8.0",
37 | "@tanstack/eslint-plugin-query": "^5.56.1",
38 | "@tanstack/router-plugin": "^1.57.13",
39 | "@types/eslint__js": "^8.42.3",
40 | "@types/node": "^22.5.5",
41 | "@types/react": "^18.3.5",
42 | "@types/react-dom": "^18.3.0",
43 | "@vitejs/plugin-react-swc": "^3.7.0",
44 | "autoprefixer": "^10.4.20",
45 | "concurrently": "^9.0.1",
46 | "eslint": "^9.10.0",
47 | "eslint-plugin-import": "^2.30.0",
48 | "eslint-plugin-jsx-a11y": "^6.10.0",
49 | "eslint-plugin-react": "^7.36.1",
50 | "eslint-plugin-react-hooks": "^4.6.2",
51 | "eslint-plugin-react-refresh": "^0.4.12",
52 | "eslint-plugin-simple-import-sort": "^12.1.1",
53 | "eslint-plugin-tailwindcss": "^3.17.4",
54 | "eslint-plugin-unicorn": "^55.0.0",
55 | "globals": "^15.9.0",
56 | "postcss": "^8.4.45",
57 | "tailwindcss": "^3.4.11",
58 | "tsx": "^4.19.1",
59 | "typescript": "^5.6.2",
60 | "typescript-eslint": "^8.5.0",
61 | "vite": "^5.4.5",
62 | "vite-tsconfig-paths": "^5.0.1"
63 | }
64 | }
--------------------------------------------------------------------------------
/csr-jwt/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/csr-jwt/scripts/build.ts:
--------------------------------------------------------------------------------
1 | import concurrently from 'concurrently'
2 |
3 | concurrently(
4 | [
5 | {
6 | name: 'client',
7 | prefixColor: '#58c4dc',
8 | command: 'pnpm client:build',
9 | },
10 | ],
11 | )
12 |
--------------------------------------------------------------------------------
/csr-jwt/scripts/dev.ts:
--------------------------------------------------------------------------------
1 | import concurrently from 'concurrently'
2 |
3 | concurrently(
4 | [
5 | {
6 | name: 'client',
7 | prefixColor: '#58c4dc',
8 | command: 'pnpm client:dev',
9 | },
10 | {
11 | name: 'server',
12 | prefixColor: '#fe6f32',
13 | command: 'pnpm server:dev',
14 | },
15 | ],
16 | )
17 |
--------------------------------------------------------------------------------
/csr-jwt/scripts/start.ts:
--------------------------------------------------------------------------------
1 | import concurrently from 'concurrently'
2 |
3 | concurrently(
4 | [
5 | {
6 | name: 'client',
7 | prefixColor: '#58c4dc',
8 | command: 'pnpm client:start',
9 | },
10 | {
11 | name: 'server',
12 | prefixColor: '#fe6f32',
13 | command: 'pnpm server:start',
14 | },
15 | ],
16 | )
17 |
--------------------------------------------------------------------------------
/csr-jwt/src/api/auth-query.ts:
--------------------------------------------------------------------------------
1 | import { queryOptions, useQuery } from '@tanstack/react-query'
2 |
3 | import { ky } from '~/lib/ky-with-auth'
4 | import type { ResAuth } from '~/types/response'
5 |
6 | export function authQueryOptions() {
7 | return queryOptions({
8 | queryKey: ['auth'],
9 | queryFn: () => ky.get('auth').json(),
10 | })
11 | }
12 |
13 | export function useAuthQuery() {
14 | return useQuery(authQueryOptions())
15 | }
16 |
--------------------------------------------------------------------------------
/csr-jwt/src/api/sign-in-mutation.ts:
--------------------------------------------------------------------------------
1 | import { useMutation, useQueryClient } from '@tanstack/react-query'
2 |
3 | import { ACCESS_TOKEN } from '~/lib/constants'
4 | import { ky } from '~/lib/ky-with-auth'
5 | import type { SignInFormSchema } from '~/lib/schema'
6 | import type { ResSignIn } from '~/types/response'
7 |
8 | export function useSignInMutation() {
9 | const queryClient = useQueryClient()
10 |
11 | return useMutation({
12 | mutationKey: ['sign-in'],
13 | mutationFn: async ({ username, password }: SignInFormSchema) => {
14 | return ky
15 | .post('sign-in', {
16 | json: {
17 | username,
18 | password,
19 | },
20 | })
21 | .json()
22 | },
23 | onSuccess: (data) => {
24 | queryClient.setQueryData(['auth'], { user: data.user })
25 | sessionStorage.setItem(ACCESS_TOKEN, data.accessToken)
26 | },
27 | onError: () => {
28 | sessionStorage.removeItem(ACCESS_TOKEN)
29 | },
30 | })
31 | }
32 |
--------------------------------------------------------------------------------
/csr-jwt/src/api/sign-out-mutation.ts:
--------------------------------------------------------------------------------
1 | import { useMutation, useQueryClient } from '@tanstack/react-query'
2 |
3 | import { ACCESS_TOKEN } from '~/lib/constants'
4 | import { ky } from '~/lib/ky-with-auth'
5 | import type { ResSignOut } from '~/types/response'
6 |
7 | export function useSignOutMutation() {
8 | const queryClient = useQueryClient()
9 |
10 | return useMutation({
11 | mutationKey: ['sign-out'],
12 | mutationFn: async () => {
13 | return ky
14 | .post('sign-out')
15 | .json()
16 | },
17 | onSuccess: () => {
18 | queryClient.setQueryData(['auth'], null)
19 | sessionStorage.removeItem(ACCESS_TOKEN)
20 | },
21 | })
22 | }
23 |
--------------------------------------------------------------------------------
/csr-jwt/src/app.tsx:
--------------------------------------------------------------------------------
1 | import { QueryClientProvider } from '@tanstack/react-query'
2 | import { RouterProvider } from '@tanstack/react-router'
3 |
4 | import { useAuth } from '~/hooks/use-auth'
5 | import { queryClient } from '~/lib/query'
6 | import { router } from '~/lib/router'
7 |
8 | function App() {
9 | return (
10 |
11 |
12 |
13 | )
14 | }
15 |
16 | function RouterProviderWithContext() {
17 | const auth = useAuth()
18 |
19 | return
20 | }
21 |
22 | export { App }
23 |
--------------------------------------------------------------------------------
/csr-jwt/src/components/navigation.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from '@tanstack/react-router'
2 |
3 | import { useAuth } from '~/hooks/use-auth'
4 | import type { FileRouteTypes } from '~/route-tree.gen'
5 |
6 | type NavigationItem = {
7 | to: FileRouteTypes['to']
8 | name: string
9 | }
10 |
11 | const navigation: NavigationItem[] = [
12 | {
13 | to: '/',
14 | name: 'Home',
15 | },
16 | {
17 | to: '/secret',
18 | name: 'Secret',
19 | },
20 | ]
21 |
22 | export function Navigation() {
23 | const auth = useAuth()
24 |
25 | return (
26 |
58 | )
59 | }
60 |
--------------------------------------------------------------------------------
/csr-jwt/src/hooks/use-auth.ts:
--------------------------------------------------------------------------------
1 | import { useQueryClient } from '@tanstack/react-query'
2 | import { useEffect } from 'react'
3 |
4 | import { authQueryOptions, useAuthQuery } from '~/api/auth-query'
5 | import { useSignOutMutation } from '~/api/sign-out-mutation'
6 | import { router } from '~/lib/router'
7 | import type { ResAuth } from '~/types/response'
8 |
9 | type AuthState =
10 | | { user: null; status: 'PENDING' }
11 | | { user: null; status: 'UNAUTHENTICATED' }
12 | | { user: ResAuth['user']; status: 'AUTHENTICATED' }
13 |
14 | type AuthUtils = {
15 | signIn: () => void
16 | signOut: () => void
17 | ensureData: () => Promise
18 | }
19 |
20 | type AuthData = AuthState & AuthUtils
21 |
22 | function useAuth(): AuthData {
23 | const authQuery = useAuthQuery()
24 | const signOutMutation = useSignOutMutation()
25 |
26 | const queryClient = useQueryClient()
27 |
28 | useEffect(() => {
29 | router.invalidate()
30 | }, [authQuery.data])
31 |
32 | useEffect(() => {
33 | if (authQuery.error === null) return
34 | queryClient.setQueryData(['auth'], null)
35 | }, [authQuery.error, queryClient])
36 |
37 | const utils: AuthUtils = {
38 | signIn: () => {
39 | router.navigate({ to: '/sign-in' })
40 | },
41 | signOut: () => {
42 | signOutMutation.mutate()
43 | },
44 | ensureData: () => {
45 | return queryClient.ensureQueryData(
46 | authQueryOptions(),
47 | )
48 | },
49 | }
50 |
51 | switch (true) {
52 | case authQuery.isPending:
53 | return { ...utils, user: null, status: 'PENDING' }
54 |
55 | case !authQuery.data:
56 | return { ...utils, user: null, status: 'UNAUTHENTICATED' }
57 |
58 | default:
59 | return { ...utils, user: authQuery.data.user, status: 'AUTHENTICATED' }
60 | }
61 | }
62 |
63 | export { useAuth }
64 | export type { AuthData }
65 |
--------------------------------------------------------------------------------
/csr-jwt/src/lib/constants.ts:
--------------------------------------------------------------------------------
1 | export const ACCESS_TOKEN = 'access-token'
2 |
--------------------------------------------------------------------------------
/csr-jwt/src/lib/ky-with-auth.ts:
--------------------------------------------------------------------------------
1 | import ky, { HTTPError } from 'ky'
2 |
3 | import { ACCESS_TOKEN } from '~/lib/constants'
4 | import type { ResRefresh } from '~/types/response'
5 |
6 | const kyWithAuth = ky.create({
7 | prefixUrl: 'http://localhost:6969',
8 | credentials: 'include',
9 | hooks: {
10 | beforeRequest: [
11 | (request) => {
12 | const accessToken = sessionStorage.getItem(ACCESS_TOKEN)
13 | if (!accessToken) return
14 |
15 | request.headers.set('Authorization', `Bearer ${accessToken}`)
16 | },
17 | ],
18 | beforeRetry: [
19 | async ({ request, error, options }) => {
20 | if (error instanceof HTTPError && error.response.status === 401) {
21 | const data = await ky
22 | .get('refresh', { ...options, retry: 0 })
23 | .json()
24 |
25 | const newAccessToken = data.accessToken
26 |
27 | sessionStorage.setItem(ACCESS_TOKEN, newAccessToken)
28 |
29 | request.headers.set('Authorization', `Bearer ${newAccessToken}`)
30 | }
31 | },
32 | ],
33 | },
34 | retry: {
35 | limit: 1,
36 | statusCodes: [401, 403, 500, 504],
37 | },
38 | })
39 |
40 | export { kyWithAuth as ky }
41 |
--------------------------------------------------------------------------------
/csr-jwt/src/lib/query.ts:
--------------------------------------------------------------------------------
1 | import { QueryClient } from '@tanstack/react-query'
2 |
3 | export const queryClient = new QueryClient({
4 | defaultOptions: {
5 | queries: {
6 | retry: false,
7 | },
8 | mutations: {
9 | retry: false,
10 | },
11 | },
12 | })
13 |
--------------------------------------------------------------------------------
/csr-jwt/src/lib/router.tsx:
--------------------------------------------------------------------------------
1 | import { createRouter } from '@tanstack/react-router'
2 | import type { QueryClient } from '@tanstack/react-query'
3 |
4 | import { queryClient } from '~/lib/query'
5 | import { routeTree } from '~/route-tree.gen'
6 | import type { AuthData } from '~/hooks/use-auth'
7 |
8 | type RouterContext = {
9 | auth: AuthData
10 | queryClient: QueryClient
11 | }
12 |
13 | const router = createRouter({
14 | routeTree,
15 | defaultPreload: 'intent',
16 | context: {
17 | auth: null as unknown as AuthData,
18 | queryClient,
19 | },
20 | })
21 |
22 | declare module '@tanstack/react-router' {
23 | interface Register {
24 | router: typeof router
25 | }
26 | }
27 |
28 | export { router }
29 | export type { RouterContext }
30 |
--------------------------------------------------------------------------------
/csr-jwt/src/lib/schema.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 |
3 | export const signInFormSchema = z.object({
4 | username: z.string(),
5 | password: z.string(),
6 | })
7 |
8 | export type SignInFormSchema = z.infer
9 |
--------------------------------------------------------------------------------
/csr-jwt/src/main.tsx:
--------------------------------------------------------------------------------
1 | import '~/styles/global.css'
2 |
3 | import { createRoot } from 'react-dom/client'
4 |
5 | import { App } from '~/app'
6 |
7 | declare global {
8 | interface Window {
9 | readonly app: HTMLElement
10 | }
11 | }
12 |
13 | createRoot(window.app).render()
14 |
--------------------------------------------------------------------------------
/csr-jwt/src/route-tree.gen.ts:
--------------------------------------------------------------------------------
1 | /* prettier-ignore-start */
2 |
3 | /* eslint-disable */
4 |
5 | // @ts-nocheck
6 |
7 | // noinspection JSUnusedGlobalSymbols
8 |
9 | // This file is auto-generated by TanStack Router
10 |
11 | // Import Routes
12 |
13 | import { Route as rootRoute } from './routes/__root'
14 | import { Route as AuthenticatedImport } from './routes/_authenticated'
15 | import { Route as AuthImport } from './routes/_auth'
16 | import { Route as IndexImport } from './routes/index'
17 | import { Route as AuthenticatedSecretImport } from './routes/_authenticated.secret'
18 | import { Route as AuthSignInImport } from './routes/_auth.sign-in'
19 |
20 | // Create/Update Routes
21 |
22 | const AuthenticatedRoute = AuthenticatedImport.update({
23 | id: '/_authenticated',
24 | getParentRoute: () => rootRoute,
25 | } as any)
26 |
27 | const AuthRoute = AuthImport.update({
28 | id: '/_auth',
29 | getParentRoute: () => rootRoute,
30 | } as any)
31 |
32 | const IndexRoute = IndexImport.update({
33 | path: '/',
34 | getParentRoute: () => rootRoute,
35 | } as any)
36 |
37 | const AuthenticatedSecretRoute = AuthenticatedSecretImport.update({
38 | path: '/secret',
39 | getParentRoute: () => AuthenticatedRoute,
40 | } as any)
41 |
42 | const AuthSignInRoute = AuthSignInImport.update({
43 | path: '/sign-in',
44 | getParentRoute: () => AuthRoute,
45 | } as any)
46 |
47 | // Populate the FileRoutesByPath interface
48 |
49 | declare module '@tanstack/react-router' {
50 | interface FileRoutesByPath {
51 | '/': {
52 | id: '/'
53 | path: '/'
54 | fullPath: '/'
55 | preLoaderRoute: typeof IndexImport
56 | parentRoute: typeof rootRoute
57 | }
58 | '/_auth': {
59 | id: '/_auth'
60 | path: ''
61 | fullPath: ''
62 | preLoaderRoute: typeof AuthImport
63 | parentRoute: typeof rootRoute
64 | }
65 | '/_authenticated': {
66 | id: '/_authenticated'
67 | path: ''
68 | fullPath: ''
69 | preLoaderRoute: typeof AuthenticatedImport
70 | parentRoute: typeof rootRoute
71 | }
72 | '/_auth/sign-in': {
73 | id: '/_auth/sign-in'
74 | path: '/sign-in'
75 | fullPath: '/sign-in'
76 | preLoaderRoute: typeof AuthSignInImport
77 | parentRoute: typeof AuthImport
78 | }
79 | '/_authenticated/secret': {
80 | id: '/_authenticated/secret'
81 | path: '/secret'
82 | fullPath: '/secret'
83 | preLoaderRoute: typeof AuthenticatedSecretImport
84 | parentRoute: typeof AuthenticatedImport
85 | }
86 | }
87 | }
88 |
89 | // Create and export the route tree
90 |
91 | interface AuthRouteChildren {
92 | AuthSignInRoute: typeof AuthSignInRoute
93 | }
94 |
95 | const AuthRouteChildren: AuthRouteChildren = {
96 | AuthSignInRoute: AuthSignInRoute,
97 | }
98 |
99 | const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
100 |
101 | interface AuthenticatedRouteChildren {
102 | AuthenticatedSecretRoute: typeof AuthenticatedSecretRoute
103 | }
104 |
105 | const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
106 | AuthenticatedSecretRoute: AuthenticatedSecretRoute,
107 | }
108 |
109 | const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren(
110 | AuthenticatedRouteChildren,
111 | )
112 |
113 | export interface FileRoutesByFullPath {
114 | '/': typeof IndexRoute
115 | '': typeof AuthenticatedRouteWithChildren
116 | '/sign-in': typeof AuthSignInRoute
117 | '/secret': typeof AuthenticatedSecretRoute
118 | }
119 |
120 | export interface FileRoutesByTo {
121 | '/': typeof IndexRoute
122 | '': typeof AuthenticatedRouteWithChildren
123 | '/sign-in': typeof AuthSignInRoute
124 | '/secret': typeof AuthenticatedSecretRoute
125 | }
126 |
127 | export interface FileRoutesById {
128 | __root__: typeof rootRoute
129 | '/': typeof IndexRoute
130 | '/_auth': typeof AuthRouteWithChildren
131 | '/_authenticated': typeof AuthenticatedRouteWithChildren
132 | '/_auth/sign-in': typeof AuthSignInRoute
133 | '/_authenticated/secret': typeof AuthenticatedSecretRoute
134 | }
135 |
136 | export interface FileRouteTypes {
137 | fileRoutesByFullPath: FileRoutesByFullPath
138 | fullPaths: '/' | '' | '/sign-in' | '/secret'
139 | fileRoutesByTo: FileRoutesByTo
140 | to: '/' | '' | '/sign-in' | '/secret'
141 | id:
142 | | '__root__'
143 | | '/'
144 | | '/_auth'
145 | | '/_authenticated'
146 | | '/_auth/sign-in'
147 | | '/_authenticated/secret'
148 | fileRoutesById: FileRoutesById
149 | }
150 |
151 | export interface RootRouteChildren {
152 | IndexRoute: typeof IndexRoute
153 | AuthRoute: typeof AuthRouteWithChildren
154 | AuthenticatedRoute: typeof AuthenticatedRouteWithChildren
155 | }
156 |
157 | const rootRouteChildren: RootRouteChildren = {
158 | IndexRoute: IndexRoute,
159 | AuthRoute: AuthRouteWithChildren,
160 | AuthenticatedRoute: AuthenticatedRouteWithChildren,
161 | }
162 |
163 | export const routeTree = rootRoute
164 | ._addFileChildren(rootRouteChildren)
165 | ._addFileTypes()
166 |
167 | /* prettier-ignore-end */
168 |
169 | /* ROUTE_MANIFEST_START
170 | {
171 | "routes": {
172 | "__root__": {
173 | "filePath": "__root.tsx",
174 | "children": [
175 | "/",
176 | "/_auth",
177 | "/_authenticated"
178 | ]
179 | },
180 | "/": {
181 | "filePath": "index.tsx"
182 | },
183 | "/_auth": {
184 | "filePath": "_auth.tsx",
185 | "children": [
186 | "/_auth/sign-in"
187 | ]
188 | },
189 | "/_authenticated": {
190 | "filePath": "_authenticated.tsx",
191 | "children": [
192 | "/_authenticated/secret"
193 | ]
194 | },
195 | "/_auth/sign-in": {
196 | "filePath": "_auth.sign-in.tsx",
197 | "parent": "/_auth"
198 | },
199 | "/_authenticated/secret": {
200 | "filePath": "_authenticated.secret.tsx",
201 | "parent": "/_authenticated"
202 | }
203 | }
204 | }
205 | ROUTE_MANIFEST_END */
206 |
--------------------------------------------------------------------------------
/csr-jwt/src/routes/__root.tsx:
--------------------------------------------------------------------------------
1 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
2 | import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'
3 | import { TanStackRouterDevtools } from '@tanstack/router-devtools'
4 |
5 | import { Navigation } from '~/components/navigation'
6 | import type { RouterContext } from '~/lib/router'
7 |
8 | export const Route = createRootRouteWithContext()({
9 | component: RootComponent,
10 | })
11 |
12 | function RootComponent() {
13 | return (
14 | <>
15 |
16 |
17 |
18 |
19 |
20 |
21 | >
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/csr-jwt/src/routes/_auth.sign-in.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute } from '@tanstack/react-router'
2 | import type { FormEvent } from 'react'
3 |
4 | import { useSignInMutation } from '~/api/sign-in-mutation'
5 | import { signInFormSchema } from '~/lib/schema'
6 |
7 | export const Route = createFileRoute('/_auth/sign-in')({
8 | component: SignInComponent,
9 | })
10 |
11 | function SignInComponent() {
12 | const signInMutation = useSignInMutation()
13 |
14 | async function handleSubmit(e: FormEvent) {
15 | e.preventDefault()
16 |
17 | const form = e.currentTarget
18 | const formData = new FormData(form)
19 | const data = Object.fromEntries(formData)
20 | const dataParsed = signInFormSchema.parse(data)
21 |
22 | await signInMutation.mutateAsync(dataParsed)
23 | }
24 |
25 | return (
26 | <>
27 |
35 | {signInMutation.error && {signInMutation.error.message}
}
36 | >
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/csr-jwt/src/routes/_auth.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
2 | import { z } from 'zod'
3 |
4 | export const Route = createFileRoute('/_auth')({
5 | validateSearch: z.object({
6 | redirect: z.string().optional().catch(''),
7 | }),
8 | beforeLoad: ({ context, search }) => {
9 | if (context.auth.status === 'AUTHENTICATED') {
10 | throw redirect({
11 | to: search.redirect || '/',
12 | })
13 | }
14 | },
15 | component: AuthLayout,
16 | })
17 |
18 | function AuthLayout() {
19 | return (
20 | <>
21 |
22 | >
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/csr-jwt/src/routes/_authenticated.secret.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute } from '@tanstack/react-router'
2 |
3 | export const Route = createFileRoute('/_authenticated/secret')({
4 | component: SecretRoute,
5 | })
6 |
7 | function SecretRoute() {
8 | return (
9 | <>
10 | This is a secret page
11 | >
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/csr-jwt/src/routes/_authenticated.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
2 |
3 | export const Route = createFileRoute('/_authenticated')({
4 | beforeLoad: async ({ context, location }) => {
5 | let shouldRedirect = false
6 |
7 | if (context.auth.status === 'PENDING') {
8 | try {
9 | await context.auth.ensureData()
10 | }
11 | catch (_) {
12 | shouldRedirect = true
13 | }
14 | }
15 |
16 | if (context.auth.status === 'UNAUTHENTICATED') {
17 | shouldRedirect = true
18 | }
19 |
20 | if (shouldRedirect) {
21 | throw redirect({
22 | to: '/sign-in',
23 | search: {
24 | redirect: location.href,
25 | },
26 | })
27 | }
28 | },
29 | component: AuthenticatedLayout,
30 | })
31 |
32 | function AuthenticatedLayout() {
33 | return (
34 | <>
35 |
36 | >
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/csr-jwt/src/routes/index.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute } from '@tanstack/react-router'
2 |
3 | import { useAuth } from '~/hooks/use-auth'
4 |
5 | export const Route = createFileRoute('/')({
6 | component: HomeComponent,
7 | })
8 |
9 | function HomeComponent() {
10 | const auth = useAuth()
11 |
12 | return (
13 | <>
14 | Welcome {auth.user?.name || 'Guest'}!
15 | >
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/csr-jwt/src/server.ts:
--------------------------------------------------------------------------------
1 | import { serve } from '@hono/node-server'
2 | import { zValidator } from '@hono/zod-validator'
3 | import { hash as hashPassword, verify as verifyPassword } from '@node-rs/argon2'
4 | import { Hono } from 'hono'
5 | import { deleteCookie, getCookie, setCookie } from 'hono/cookie'
6 | import { cors } from 'hono/cors'
7 | import { jwt, sign, verify } from 'hono/jwt'
8 | import { JwtTokenExpired, JwtTokenInvalid } from 'hono/utils/jwt/types'
9 | import { z } from 'zod'
10 |
11 | const ACCESS_TOKEN_EXP = 60 * 5
12 | const REFRESH_TOKEN_EXP = 60 * 60 * 24 * 7
13 | const ACCESS_TOKEN_SECRET = '/tSyaxS2fZu0Y4Ry6iZUwcJsoLFUlHP+dtf24JMamuA='
14 | const REFRESH_TOKEN_SECRET = 'MgWQE0MTB9RlnHFPeGEcup+s4E9+CiQtrzNyHd/P2hA='
15 | const REFRESH_TOKEN_COOKIE = 'refresh-token'
16 |
17 | type JWTPayload = {
18 | id: number
19 | username: string
20 | }
21 |
22 | type Variables = {
23 | jwtPayload: JWTPayload
24 | }
25 |
26 | const app = new Hono<{ Variables: Variables }>()
27 | const port = 6969
28 |
29 | const db = {
30 | users: [
31 | { id: 1, username: 'admin', password: await hashPassword('admin'), name: 'John Doe' },
32 | ],
33 | }
34 |
35 | app.use('*', cors({
36 | origin: 'http://localhost:5173',
37 | credentials: true,
38 | }))
39 |
40 | app.get('/', (c) => {
41 | return c.text('Hello Hono!')
42 | })
43 |
44 | app.post(
45 | '/sign-in',
46 | zValidator(
47 | 'json',
48 | z.object({
49 | username: z.string(),
50 | password: z.string(),
51 | }),
52 | async (result, c) => {
53 | if (!result.success) return c.json(result.error, 400)
54 |
55 | const { username, password } = result.data
56 |
57 | const incorrectResponse = c.json(
58 | { message: 'username or password is incorrect' },
59 | 401,
60 | )
61 |
62 | const user = db.users.find((user) => user.username === username)
63 | if (!user) return incorrectResponse
64 |
65 | const isPasswordMatch = await verifyPassword(user.password, password)
66 | if (!isPasswordMatch) return incorrectResponse
67 |
68 | const payload: JWTPayload = { id: user.id, username }
69 |
70 | const accessToken = await generateAccessToken(payload, ACCESS_TOKEN_EXP)
71 | const refreshToken = await generateRefreshToken(payload, REFRESH_TOKEN_EXP)
72 |
73 | setCookie(c, REFRESH_TOKEN_COOKIE, refreshToken, {
74 | secure: true,
75 | httpOnly: true,
76 | sameSite: 'strict',
77 | maxAge: REFRESH_TOKEN_EXP,
78 | })
79 |
80 | return c.json({ accessToken, user: { name: user.name } })
81 | },
82 | ),
83 | )
84 |
85 | app.post('/sign-out', (c) => {
86 | deleteCookie(c, REFRESH_TOKEN_COOKIE)
87 |
88 | return c.json({ message: 'success' })
89 | })
90 |
91 | app.get('/refresh', async (c) => {
92 | const refreshToken = getCookie(c, REFRESH_TOKEN_COOKIE)
93 | if (!refreshToken) return c.json({ message: 'refresh token not found' }, 401)
94 |
95 | try {
96 | const decoded = await verify(refreshToken, REFRESH_TOKEN_SECRET) as JWTPayload
97 | const payload: JWTPayload = { id: decoded.id, username: decoded.username }
98 | const accessToken = await generateAccessToken(payload, ACCESS_TOKEN_EXP)
99 |
100 | return c.json({ accessToken })
101 |
102 | }
103 | catch (error) {
104 | switch (true) {
105 | case error instanceof JwtTokenExpired:
106 | return c.json({ message: 'refresh token expired' }, 401)
107 |
108 | case error instanceof JwtTokenInvalid:
109 | return c.json({ message: 'refresh token invalid' }, 401)
110 |
111 | default:
112 | return c.json({ message: 'unexpected error', error }, 401)
113 | }
114 | }
115 | })
116 |
117 | app.use(
118 | '/auth/*',
119 | jwt({
120 | secret: ACCESS_TOKEN_SECRET,
121 | }),
122 | )
123 |
124 | app.get('/auth', (c) => {
125 | const payload = c.get('jwtPayload')
126 |
127 | const user = db.users.find((user) => user.id === payload.id)
128 | if (!user) return c.json({ message: 'user not found' }, 404)
129 |
130 | return c.json({
131 | user: {
132 | name: user.name,
133 | },
134 | })
135 | })
136 |
137 | serve({
138 | fetch: app.fetch,
139 | port,
140 | })
141 |
142 | console.log(`Server is running at http://localhost:${port}`)
143 |
144 | function generateAccessToken(payload: JWTPayload, exp: number) {
145 | return sign(
146 | {
147 | ...payload,
148 | exp: createExpiresAt(exp),
149 | },
150 | ACCESS_TOKEN_SECRET,
151 | )
152 | }
153 |
154 | function generateRefreshToken(payload: JWTPayload, exp: number) {
155 | return sign(
156 | {
157 | ...payload,
158 | exp: createExpiresAt(exp),
159 | },
160 | REFRESH_TOKEN_SECRET,
161 | )
162 | }
163 |
164 | function createExpiresAt(time: number) {
165 | return Math.floor(Date.now() / 1000) + time
166 | }
167 |
--------------------------------------------------------------------------------
/csr-jwt/src/styles/global.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/csr-jwt/src/types/response.ts:
--------------------------------------------------------------------------------
1 | type User = {
2 | name: string
3 | }
4 |
5 | export type ResSignIn = {
6 | accessToken: string
7 | user: User
8 | }
9 |
10 | export type ResSignOut = {
11 | message: string
12 | }
13 |
14 | export type ResRefresh = {
15 | accessToken: string
16 | }
17 |
18 | export type ResAuth = {
19 | user: User
20 | }
21 |
--------------------------------------------------------------------------------
/csr-jwt/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss'
2 |
3 | export default {
4 | content: [
5 | './index.html',
6 | './src/**/*.tsx',
7 | ],
8 | theme: {
9 | extend: {},
10 | },
11 | plugins: [],
12 | } satisfies Config
13 |
--------------------------------------------------------------------------------
/csr-jwt/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "skipLibCheck": true,
5 | "esModuleInterop": true,
6 | "jsx": "react-jsx",
7 | "target": "ESNext",
8 | "module": "ESNext",
9 | "moduleResolution": "Bundler",
10 | "baseUrl": ".",
11 | "paths": {
12 | "~/*": ["./src/*"]
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/csr-jwt/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
2 | import react from '@vitejs/plugin-react-swc'
3 | import { defineConfig } from 'vite'
4 | import tsconfigPaths from 'vite-tsconfig-paths'
5 |
6 | export default defineConfig({
7 | plugins: [
8 | TanStackRouterVite({
9 | generatedRouteTree: 'src/route-tree.gen.ts',
10 | }),
11 | tsconfigPaths(),
12 | react(),
13 | ],
14 | })
15 |
--------------------------------------------------------------------------------