├── .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 |
27 | 39 | 40 | {auth.status === 'PENDING' && ( 41 |
Loading...
42 | )} 43 | 44 | {auth.status === 'UNAUTHENTICATED' && ( 45 |
46 | 47 |
48 | )} 49 | 50 | {auth.status === 'AUTHENTICATED' && ( 51 |
52 | 53 |

|

54 |
Welcome back, {auth.user.name}
55 |
56 | )} 57 |
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 |
31 | 32 | 33 | 34 |
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 |
27 | 39 | 40 | {auth.status === 'PENDING' && ( 41 |
Loading...
42 | )} 43 | 44 | {auth.status === 'UNAUTHENTICATED' && ( 45 |
46 | 47 |
48 | )} 49 | 50 | {auth.status === 'AUTHENTICATED' && ( 51 |
52 | 53 |

|

54 |
Welcome back, {auth.user.name}
55 |
56 | )} 57 |
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 |
31 | 32 | 33 | 34 |
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 | --------------------------------------------------------------------------------