├── .gitignore ├── README.md ├── lib ├── client │ ├── firebase.ts │ ├── helpers.ts │ └── hooks.ts └── server │ ├── firebase.ts │ └── helpers.ts ├── next-env.d.ts ├── package.json ├── pages ├── _app.tsx ├── api │ └── protected.ts ├── index.tsx └── protected.tsx ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | # yarn v2 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .yarn/install-state.gz 116 | .pnp.* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js TypeScript SWR Water.css Serverless Firebase Auth Template 2 | 3 | A template for a serverless Next.js site built with TypeScript, styled with Water.css, fetching data with SWR, and authenticating users with Firebase Auth and GitHub. 4 | 5 | Yes. Many technologies. But the end result is a super flexible app structure that's fast and easy to work with. 6 | 7 | The code is clean and easy to read, understand, and modify unlike many other templates. And we're nice enough to offer advice and fix things if you open an issue. 8 | 9 | ## Prerequisites 10 | 11 | - Node 12+ 12 | - Yarn (comes with most Node installations) 13 | - A Firebase project (create one [here](https://console.firebase.google.com/)) 14 | - A GitHub app linked to the Firebase project (for authentication) 15 | 16 | ## Quick Start 17 | 18 | ``` 19 | git clone https://github.com/stacc-dev/next-typescript-swr-watercss-serverless-firebase-auth-template.git template 20 | cd template/ 21 | yarn 22 | ``` 23 | 24 | Create a file called `.env` with the following environment variables: 25 | 26 | - `NEXT_PUBLIC_FIREBASE_PROJECT_ID`: The id of your project in Firebase 27 | - `NEXT_PUBLIC_FIREBASE_APP_ID`: The id of a Firebase web app (create one in the console if needed) 28 | - `NEXT_PUBLIC_FIREBASE_API_KEY`: The api key for the same web app 29 | - `GCLOUD_CREDENTIALS`: A base64-encoded JSON string containing credentials to a Firebase service account (create one in the console if needed) 30 | 31 | If you aren't familiar with `.env` files, put each environment variable on separate lines. Each line should be in the format of `VARIABLE_NAME=VARIABLE_VALUE`. Create an issue if you have any other questions! 32 | 33 | Now just run `yarn dev` to start the development server. 34 | -------------------------------------------------------------------------------- /lib/client/firebase.ts: -------------------------------------------------------------------------------- 1 | import firebase, { auth } from 'firebase/app' 2 | import 'firebase/auth' 3 | 4 | if (!process.env.NEXT_PUBLIC_FIREBASE_API_KEY) throw new Error('NEXT_PUBLIC_FIREBASE_API_KEY environment variable not provided') 5 | if (!process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID) throw new Error('NEXT_PUBLIC_FIREBASE_PROJECT_ID environment variable not provided') 6 | if (!process.env.NEXT_PUBLIC_FIREBASE_APP_ID) throw new Error('NEXT_PUBLIC_FIREBASE_APP_ID environment variable not provided') 7 | 8 | if (firebase.apps.length === 0) { 9 | firebase.initializeApp({ 10 | apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, 11 | authDomain: `${process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID}.firebaseapp.com`, 12 | databaseURL: `https://${process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID}.firebaseio.com`, 13 | projectId: process.env.NEXT_PUBLIC_IREBASE_PROJECT_ID, 14 | storageBucket: `${process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID}.appspot.com`, 15 | appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID 16 | }) 17 | } 18 | 19 | export default firebase 20 | 21 | export const authProviders = { 22 | github: new firebase.auth.GithubAuthProvider() 23 | } 24 | authProviders.github.setCustomParameters({ prompt: 'select_account' }) -------------------------------------------------------------------------------- /lib/client/helpers.ts: -------------------------------------------------------------------------------- 1 | import firebase from 'lib/client/firebase' 2 | 3 | export const loginWith = (provider: firebase.auth.AuthProvider) => async () => { 4 | const { user } = await firebase.auth().signInWithPopup(provider) 5 | return user 6 | } 7 | 8 | export const logout = () => firebase.auth().signOut() -------------------------------------------------------------------------------- /lib/client/hooks.ts: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr' 2 | import { User } from 'firebase' 3 | import Router from 'next/router' 4 | import { useEffect } from 'react' 5 | 6 | export const useRequireUser = (user: User | null, loading: boolean, redirect: string = '/') => { 7 | useEffect(() => { 8 | if (!loading && !user) Router.replace(redirect) 9 | }, [ user, loading ]) 10 | } 11 | 12 | export const useRequireNoUser = (user: User | null, loading: boolean, redirect: string = '/') => { 13 | useEffect(() => { 14 | if (!loading && user) Router.replace(redirect) 15 | }, [ user, loading ]) 16 | } 17 | 18 | export const authedDataFetcher = async (endpoint: string, user: User | null) => { 19 | if (!user) return null 20 | const idToken = await user.getIdToken() 21 | 22 | const res = await fetch(endpoint, { 23 | method: 'POST', 24 | headers: { 25 | 'Content-Type': 'application/json' 26 | }, 27 | body: JSON.stringify({ idToken }) 28 | }) 29 | const text = await res.text() 30 | 31 | if (!res.ok) { 32 | throw new Error(`Error ${res.status}: ${text}`) 33 | } 34 | 35 | try { 36 | return JSON.parse(text) 37 | } catch { 38 | throw new Error(`Error parsing JSON: ${text}`) 39 | } 40 | } 41 | 42 | export const useAuthedData = (endpoint: string, user: User | null) => { 43 | return useSWR([ endpoint, user ], authedDataFetcher) 44 | } -------------------------------------------------------------------------------- /lib/server/firebase.ts: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase-admin' 2 | 3 | if (!process.env.GCLOUD_CREDENTIALS) throw new Error('GCLOUD_CREDENTIALS environment variable not provided') 4 | if (!process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID) throw new Error('NEXT_PUBLIC_FIREBASE_PROJECT_ID environment variable not provided') 5 | 6 | if (firebase.apps.length === 0) { 7 | firebase.initializeApp({ 8 | credential: firebase.credential.cert(JSON.parse( 9 | Buffer.from(process.env.GCLOUD_CREDENTIALS as string, 'base64').toString() 10 | )), 11 | databaseURL: `https://${process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID}.firebaseio.com` 12 | }) 13 | } 14 | 15 | export default firebase -------------------------------------------------------------------------------- /lib/server/helpers.ts: -------------------------------------------------------------------------------- 1 | import admin from 'firebase-admin' 2 | import { NextApiRequest, NextApiResponse } from 'next' 3 | import firebase from 'lib/server/firebase' 4 | 5 | export const authenticate = (handler: (req: NextApiRequest, res: NextApiResponse, user: admin.auth.UserRecord) => Promise) => { 6 | return async (req: NextApiRequest, res: NextApiResponse) => { 7 | if (req.method !== 'POST') return res.status(400).send('Unsupported method') 8 | 9 | const { idToken } = req.body 10 | if (typeof idToken !== 'string') return res.status(400).send('Invalid id token') 11 | 12 | let user: admin.auth.UserRecord 13 | try { 14 | const claims = await firebase.auth().verifyIdToken(idToken, true) 15 | user = await firebase.auth().getUser(claims.uid) 16 | } catch (error) { 17 | return res.status(403).send(error.message) 18 | } 19 | 20 | await handler(req, res, user) 21 | } 22 | } -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "firebase": "^7.16.1", 4 | "firebase-admin": "^9.0.0", 5 | "next": "^9.4.4", 6 | "react": "^16.13.1", 7 | "react-dom": "^16.13.1", 8 | "react-firebase-hooks": "^2.2.0", 9 | "swr": "^0.2.3", 10 | "water.css": "^2.0.0" 11 | }, 12 | "devDependencies": { 13 | "@types/node": "^14.0.23", 14 | "@types/react": "^16.9.43", 15 | "typescript": "^3.9.7" 16 | }, 17 | "scripts": { 18 | "dev": "next" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { AppProps } from 'next/app' 2 | 3 | import 'water.css/out/dark.min.css' 4 | 5 | export default ({ Component, pageProps }: AppProps) => 6 | -------------------------------------------------------------------------------- /pages/api/protected.ts: -------------------------------------------------------------------------------- 1 | import { authenticate } from 'lib/server/helpers' 2 | 3 | export default authenticate(async (req, res, user) => { 4 | return res.status(200).json({ 5 | message: `This came from an authenticated API request by ${user.displayName}.` 6 | }) 7 | }) -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { useAuthState } from 'react-firebase-hooks/auth' 2 | import firebase, { authProviders } from 'lib/client/firebase' 3 | import { loginWith } from 'lib/client/helpers' 4 | import { useRequireNoUser } from 'lib/client/hooks' 5 | 6 | export default () => { 7 | const [ user, loading ] = useAuthState(firebase.auth()) 8 | useRequireNoUser(user, loading, '/protected') 9 | 10 | return <> 11 |

Landing Page

12 |

13 | This is the landing page for this Next.js TypeScript SWR Water.css Serverless Firebase Auth template.{' '} 14 | Click the button below to get access to the protected page. 15 |

16 | 17 | 18 | } -------------------------------------------------------------------------------- /pages/protected.tsx: -------------------------------------------------------------------------------- 1 | import { useAuthState } from 'react-firebase-hooks/auth' 2 | import firebase from 'lib/client/firebase' 3 | import { useRequireUser, useAuthedData } from 'lib/client/hooks' 4 | import { logout } from 'lib/client/helpers' 5 | 6 | export default () => { 7 | const [ user, loading ] = useAuthState(firebase.auth()) 8 | useRequireUser(user, loading) 9 | 10 | const data = useAuthedData<{ message: string }>('/api/protected', user) 11 | if (!user) return 'Loading...' 12 | 13 | return <> 14 |

Protected Page

15 |

You are logged in as {user.displayName}.

16 |

{data.error ? `Error loading data: ${data.error.message}` : data.data?.message ?? 'Loading...'}

17 | 18 | 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "baseUrl": "." 21 | }, 22 | "exclude": [ 23 | "node_modules" 24 | ], 25 | "include": [ 26 | "next-env.d.ts", 27 | "**/*.ts", 28 | "**/*.tsx" 29 | ] 30 | } 31 | --------------------------------------------------------------------------------