├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── README.md ├── jest.config.js ├── jest.setup.js ├── next-env.d.ts ├── next.config.js ├── package.json ├── public └── favicon │ ├── favicon-16x16.png │ └── favicon-32x32.png ├── src ├── components │ ├── App │ │ ├── App.tsx │ │ └── index.ts │ ├── Example │ │ ├── Example.tsx │ │ └── index.ts │ ├── Home │ │ ├── Home.tsx │ │ └── index.ts │ ├── Login │ │ ├── Login.tsx │ │ └── index.ts │ └── Private │ │ ├── Private.tsx │ │ ├── PrivateSSP.tsx │ │ └── index.ts ├── modules │ ├── app │ │ ├── hooks.ts │ │ ├── index.ts │ │ └── store.ts │ ├── auth │ │ ├── index.ts │ │ ├── reducers │ │ │ ├── index.ts │ │ │ └── slice.ts │ │ ├── selectors │ │ │ ├── index.ts │ │ │ ├── selectAuth.ts │ │ │ └── selectUser.ts │ │ ├── thunks │ │ │ ├── index.ts │ │ │ ├── login.ts │ │ │ ├── logout.ts │ │ │ └── whoami.ts │ │ └── utils │ │ │ ├── index.ts │ │ │ └── withAuthedUser.ts │ └── example │ │ ├── actions.ts │ │ ├── index.ts │ │ ├── selectors.ts │ │ └── slice.ts ├── pages │ ├── _app.ts │ ├── api │ │ └── auth │ │ │ ├── login.ts │ │ │ └── whoami.ts │ ├── example.ts │ ├── index.ts │ ├── login.ts │ ├── private-ssp.ts │ └── private.ts ├── server-utils │ ├── absoluteUrl.ts │ ├── getServerCookies.ts │ └── index.ts ├── settings │ └── index.ts ├── styles │ └── global.css └── utils │ ├── index.ts │ └── isServer.ts ├── test ├── components │ └── Home.spec.tsx └── renderWithRedux.tsx ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .yalc 2 | dist 3 | node_modules 4 | src/generated 5 | src/shared 6 | src/**/*.css.d.ts 7 | src/**/*.less.d.ts 8 | src/**/*.scss.d.ts 9 | src/react-app-env.d.ts 10 | babel.* 11 | hmr.gulpfile.js 12 | jest.* 13 | polywrapper.js 14 | purex.js 15 | starport.js 16 | webpack.* 17 | next-env.d.ts 18 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'plugin:jsx-a11y/recommended', 4 | 'plugin:import/recommended', 5 | 'react-app', 6 | 'standard-with-typescript', 7 | 'standard-react', 8 | 'plugin:prettier/recommended', 9 | 'prettier', 10 | 'plugin:jsdoc/recommended', 11 | 'plugin:markdown/recommended', 12 | ], 13 | parserOptions: { 14 | project: './tsconfig.json', 15 | }, 16 | plugins: ['@typescript-eslint', 'prettier', 'jsdoc'], 17 | rules: { 18 | 'import/no-default-export': 'off', 19 | 'jsdoc/require-jsdoc': 'off', 20 | 'no-param-reassign': ['error', { props: false }], 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/strict-boolean-expressions': 'off', 23 | 'jsx-a11y/anchor-is-valid': [ 24 | 'error', 25 | { 26 | components: ['Link'], 27 | specialLink: ['hrefLeft', 'hrefRight'], 28 | aspects: ['invalidHref', 'preferButton'], 29 | }, 30 | ], 31 | }, 32 | settings: { 33 | 'import/resolver': { 34 | node: { 35 | extensions: ['.ts', '.tsx', '.js', '.jsx'], 36 | moduleDirectory: ['node_modules', 'src/'], 37 | }, 38 | }, 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .vscode 3 | .idea 4 | 5 | # next.js build output 6 | .next 7 | 8 | # dotenv environment variables 9 | .env*.local 10 | 11 | # Dependency directories 12 | node_modules/ 13 | 14 | # misc 15 | .DS_Store 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | coverage 20 | lcov.info 21 | lerna-debug.log 22 | .yalc 23 | .eslintcache 24 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .yalc 2 | dist 3 | node_modules 4 | src/generated 5 | src/shared 6 | babel.* 7 | gulpfile.* 8 | hmr.gulpfile.js 9 | jest.* 10 | polywrapper.js 11 | purex.js 12 | starport.js 13 | webpack.* 14 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "printWidth": 100, 4 | "semi": false, 5 | "singleQuote": true, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js starter 2 | 3 | Next.js starter kit with some defaults: 4 | 5 | - [x] Redux Toolkit 6 | - [x] JWT based authentication 7 | - [x] Eslint 8 | - [ ] Jest & Testing library 9 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}"], 3 | coverageThreshold: { 4 | global: { 5 | branches: 1, 6 | functions: 1, 7 | lines: 1, 8 | statements: 1, 9 | }, 10 | }, 11 | moduleDirectories: ['src', 'node_modules'], 12 | setupFilesAfterEnv: ['/jest.setup.js'], 13 | testPathIgnorePatterns: ['/.next/', '/node_modules/'], 14 | } 15 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | future: { 3 | webpack5: true, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-starter", 3 | "version": "2.0.0", 4 | "description": "Next.js starter app", 5 | "author": "Hugo Pineda ", 6 | "license": "UNLICENSED", 7 | "scripts": { 8 | "dev": "next dev -p 3001", 9 | "build": "next build", 10 | "start": "next start -p 3001", 11 | "lint": "eslint \"**/*.{md,mjs,js,jsx,ts,tsx}\" --cache", 12 | "test": "jest" 13 | }, 14 | "dependencies": { 15 | "@reduxjs/toolkit": "^1.5.1", 16 | "@rtk-incubator/rtk-query": "^0.3.0", 17 | "bcryptjs": "^2.4.3", 18 | "js-cookie": "^2.2.1", 19 | "jsonwebtoken": "^8.5.1", 20 | "next": "^10.2.0", 21 | "next-redux-wrapper": "^6.0.2", 22 | "react": "^17.0.2", 23 | "react-dom": "^17.0.2", 24 | "react-redux": "^7.2.4", 25 | "redux": "^4.1.0", 26 | "redux-thunk": "^2.3.0" 27 | }, 28 | "devDependencies": { 29 | "@testing-library/jest-dom": "^5.12.0", 30 | "@testing-library/react": "^11.2.7", 31 | "@types/bcryptjs": "^2.4.2", 32 | "@types/js-cookie": "^2.2.6", 33 | "@types/jsonwebtoken": "^8.5.1", 34 | "@types/node": "^15.3.0", 35 | "@types/react": "^17.0.5", 36 | "@types/react-redux": "^7.1.16", 37 | "@types/redux-mock-store": "^1.0.2", 38 | "@typescript-eslint/eslint-plugin": "^4.23.0", 39 | "@typescript-eslint/parser": "^4.23.0", 40 | "babel-eslint": "^10.1.0", 41 | "eslint": "^7.26.0", 42 | "eslint-config-prettier": "^8.3.0", 43 | "eslint-config-react-app": "^6.0.0", 44 | "eslint-config-standard-react": "^11.0.1", 45 | "eslint-config-standard-with-typescript": "^20.0.0", 46 | "eslint-import-resolver-babel-module": "^5.3.1", 47 | "eslint-plugin-flowtype": "^5.7.2", 48 | "eslint-plugin-import": "^2.23.2", 49 | "eslint-plugin-jest": "^24.3.6", 50 | "eslint-plugin-jest-dom": "^3.9.0", 51 | "eslint-plugin-jsdoc": "^34.6.3", 52 | "eslint-plugin-jsx-a11y": "^6.4.1", 53 | "eslint-plugin-markdown": "^2.1.0", 54 | "eslint-plugin-node": "^11.1.0", 55 | "eslint-plugin-prettier": "^3.4.0", 56 | "eslint-plugin-promise": "^5.1.0", 57 | "eslint-plugin-react": "^7.23.2", 58 | "eslint-plugin-react-hooks": "^4.2.0", 59 | "eslint-plugin-standard": "^5.0.0", 60 | "eslint-plugin-testing-library": "^4.4.0", 61 | "jest": "^26.6.3", 62 | "prettier": "^2.3.0", 63 | "redux-mock-store": "^1.5.4", 64 | "tslint-config-prettier": "^1.18.0", 65 | "typescript": "^4.2.4" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /public/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugotox/nextjs-cookie-demo/afd0863c1b57f41b3296f36a261a97573fa255af/public/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugotox/nextjs-cookie-demo/afd0863c1b57f41b3296f36a261a97573fa255af/public/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /src/components/App/App.tsx: -------------------------------------------------------------------------------- 1 | import { AppProps } from 'next/app' 2 | import Head from 'next/head' 3 | import React from 'react' 4 | 5 | import { wrapper } from 'modules/app' 6 | 7 | const WrappedApp = ({ Component, pageProps }: AppProps) => { 8 | return ( 9 | <> 10 | 11 | Next.js Starter App 12 | 13 | 14 | 15 | 16 | 17 | ) 18 | } 19 | 20 | export const MyApp = wrapper.withRedux(WrappedApp) 21 | -------------------------------------------------------------------------------- /src/components/App/index.ts: -------------------------------------------------------------------------------- 1 | export * from './App' 2 | -------------------------------------------------------------------------------- /src/components/Example/Example.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { useAppSelector, wrapper } from 'modules/app' 4 | import { receiveExample, selectSelectExampleMessage } from 'modules/example' 5 | import { isServer } from 'utils' 6 | import Link from 'next/link' 7 | 8 | export const Example = () => { 9 | const message = useAppSelector(selectSelectExampleMessage) 10 | return ( 11 |
12 |

Example with dispatch from server side props

13 | Data from redux: 14 |
{message}
15 | 16 | Home 17 | 18 |
19 | ) 20 | } 21 | 22 | export const getServerSideProps = wrapper.getServerSideProps(({ store }) => { 23 | const { dispatch } = store 24 | const side = isServer() ? 'server' : 'client' 25 | dispatch(receiveExample({ message: `Message dispatched ${side} side` })) 26 | return { 27 | props: {}, 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /src/components/Example/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Example' 2 | -------------------------------------------------------------------------------- /src/components/Home/Home.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import React from 'react' 3 | 4 | export const Home = () => { 5 | return ( 6 | <> 7 |

Next.js Starter project

8 |

Example pages:

9 | 26 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/components/Home/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Home' 2 | -------------------------------------------------------------------------------- /src/components/Login/Login.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | 3 | import { useAppDispatch } from 'modules/app' 4 | import { login } from 'modules/auth/thunks' 5 | 6 | export const Login = () => { 7 | const [username, setUsername] = useState('test') 8 | const [password, setPassword] = useState('password') 9 | const dispatch = useAppDispatch() 10 | 11 | const handleSubmit = async (e: React.SyntheticEvent) => { 12 | e.preventDefault() 13 | dispatch(login({ username, password })).catch(() => {}) 14 | } 15 | 16 | return ( 17 |
18 |
19 |
20 | 21 | setUsername(e.target.value)} 24 | type="text" 25 | value={username} 26 | /> 27 |
28 |
29 | 30 | setPassword(e.target.value)} 33 | type="password" 34 | value={password} 35 | /> 36 |
37 |
38 | 39 |
40 |
41 | 50 |
51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /src/components/Login/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Login' 2 | -------------------------------------------------------------------------------- /src/components/Private/Private.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import React from 'react' 3 | 4 | import { useAppDispatch, useAppSelector } from 'modules/app' 5 | import { selectUser, logout } from 'modules/auth' 6 | 7 | export const Private = () => { 8 | const user = useAppSelector(selectUser) 9 | const dispatch = useAppDispatch() 10 | 11 | const handleLogout = () => { 12 | dispatch(logout()) 13 | } 14 | 15 | return ( 16 |
17 |

Private page example

18 | User:
{JSON.stringify(user, null, 2)}
19 |
20 | 21 | Home 22 | 23 |
24 | 25 | 26 | Logout 27 | 28 | 29 | 34 |
35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /src/components/Private/PrivateSSP.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import React from 'react' 3 | 4 | import { useAppDispatch, useAppSelector } from 'modules/app' 5 | import { selectUser, logout } from 'modules/auth' 6 | 7 | export interface Props { 8 | message?: string 9 | } 10 | 11 | export const PrivateSSP = ({ message }: Props) => { 12 | const user = useAppSelector(selectUser) 13 | const dispatch = useAppDispatch() 14 | 15 | const handleLogout = () => { 16 | dispatch(logout()) 17 | } 18 | 19 | return ( 20 |
21 |

Private page example with server side props

22 | User:
{JSON.stringify(user, null, 2)}
23 |
24 | Server side props: 25 |
{message}
26 |
27 | 28 | Home 29 | 30 |
31 | 32 | 33 | Logout 34 | 35 | 36 |
37 | 42 |
43 | ) 44 | } 45 | 46 | export const getServerSideProps = () => { 47 | return { 48 | props: { message: 'Message from getServerSideProps' }, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/components/Private/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Private' 2 | export * from './PrivateSSP' 3 | -------------------------------------------------------------------------------- /src/modules/app/hooks.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' 2 | 3 | import { RootState, AppDispatch } from './store' 4 | 5 | // Use throughout your app instead of plain `useDispatch` and `useSelector` 6 | export const useAppDispatch = () => useDispatch() 7 | export const useAppSelector: TypedUseSelectorHook = useSelector 8 | -------------------------------------------------------------------------------- /src/modules/app/index.ts: -------------------------------------------------------------------------------- 1 | export * from './store' 2 | export * from './hooks' 3 | -------------------------------------------------------------------------------- /src/modules/app/store.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction, combineReducers, configureStore } from '@reduxjs/toolkit' 2 | import { createWrapper, Context, HYDRATE } from 'next-redux-wrapper' 3 | import { ThunkDispatch } from 'redux-thunk' 4 | 5 | import { reducer as auth } from 'modules/auth' 6 | import { reducer as example } from 'modules/example' 7 | 8 | const combinedReducers = combineReducers({ 9 | auth, 10 | example, 11 | }) 12 | 13 | export type RootState = ReturnType 14 | 15 | const rootReducer: any = (state: RootState, action: any = {}) => { 16 | if (action.type === HYDRATE) { 17 | // Attention! This will overwrite client state! Real apps should use proper reconciliation. 18 | const nextState = { 19 | ...state, // use previous state 20 | ...action.payload, // apply delta from hydration 21 | } 22 | return nextState 23 | } else { 24 | return combinedReducers(state, action) 25 | } 26 | } 27 | 28 | // create a makeStore function 29 | const makeStore = (context: Context) => { 30 | // default middleware in dev mode: [thunk, immutableStateInvariant, serializableStateInvariant] 31 | // default middleware in prod mode: [thunk] 32 | // https://redux-toolkit.js.org/api/getDefaultMiddleware#included-default-middleware 33 | return configureStore({ 34 | reducer: rootReducer, 35 | }) 36 | } 37 | 38 | export type AppDispatch = ThunkDispatch 39 | 40 | export type AppStore = Omit, 'dispatch'> & { 41 | dispatch: AppDispatch 42 | } 43 | 44 | // export type AppThunk = ThunkAction 45 | 46 | // export an assembled wrapper 47 | export const wrapper = createWrapper(makeStore, { 48 | debug: false, // NEXT_PUBLIC_NODE_ENV === 'development', 49 | }) 50 | -------------------------------------------------------------------------------- /src/modules/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from './reducers' 2 | export * from './selectors' 3 | export * from './thunks' 4 | export * from './utils' 5 | -------------------------------------------------------------------------------- /src/modules/auth/reducers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './slice' 2 | -------------------------------------------------------------------------------- /src/modules/auth/reducers/slice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit' 2 | 3 | export interface UserType { 4 | username?: string 5 | firstName?: string 6 | lastName?: string 7 | email?: string 8 | } 9 | export interface AuthSliceType { 10 | user: UserType | null 11 | } 12 | 13 | export const initialState: AuthSliceType = { 14 | user: null, 15 | } 16 | 17 | const AuthSlice = createSlice({ 18 | name: 'auth', 19 | initialState, 20 | reducers: { 21 | receiveAuthData: (state, action: PayloadAction) => { 22 | const { user } = action.payload 23 | state.user = user 24 | }, 25 | }, 26 | }) 27 | 28 | export const { receiveAuthData } = AuthSlice.actions 29 | export const { reducer } = AuthSlice 30 | -------------------------------------------------------------------------------- /src/modules/auth/selectors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './selectAuth' 2 | export * from './selectUser' 3 | -------------------------------------------------------------------------------- /src/modules/auth/selectors/selectAuth.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from 'modules/app' 2 | 3 | export const selectAuth = (state: RootState) => { 4 | return state.auth 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/auth/selectors/selectUser.ts: -------------------------------------------------------------------------------- 1 | import { compose } from 'redux' 2 | 3 | import { selectAuth } from 'modules/auth/selectors' 4 | 5 | export const selectUser = compose((auth) => { 6 | return auth.user 7 | }, selectAuth) 8 | -------------------------------------------------------------------------------- /src/modules/auth/thunks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login' 2 | export * from './logout' 3 | export * from './whoami' 4 | -------------------------------------------------------------------------------- /src/modules/auth/thunks/login.ts: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | import Router from 'next/router' 3 | 4 | import { receiveAuthData } from 'modules/auth' 5 | import { AppDispatch } from 'modules/app' 6 | 7 | interface Login { 8 | username: string 9 | password: string 10 | } 11 | 12 | export const login = ({ username, password }: Login) => async (dispatch: AppDispatch) => { 13 | try { 14 | const response = await fetch('/api/auth/login', { 15 | method: 'POST', 16 | body: JSON.stringify({ username, password }), 17 | headers: { 18 | 'Content-Type': 'application/json', 19 | }, 20 | }) 21 | const data = await response.json() 22 | if (response.status === 200 && data?.user && data?.token) { 23 | // TODO: validate `data` that comes from the server is a `UserType` data type 24 | dispatch(receiveAuthData({ user: data.user })) 25 | 26 | Cookies.set('token', data.token) 27 | 28 | const path = Router.query.next ? String(Router.query.next) : '/' 29 | Router.replace(path).catch(() => { 30 | window.location.href = path 31 | }) 32 | } 33 | } catch {} 34 | } 35 | -------------------------------------------------------------------------------- /src/modules/auth/thunks/logout.ts: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | import { Dispatch } from 'redux' 3 | import { receiveAuthData, initialState } from 'modules/auth' 4 | 5 | export const logout = () => (dispatch: Dispatch) => { 6 | Cookies.remove('token') 7 | dispatch(receiveAuthData(initialState)) 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/auth/thunks/whoami.ts: -------------------------------------------------------------------------------- 1 | import { AppDispatch } from 'modules/app' 2 | import { receiveAuthData } from 'modules/auth' 3 | 4 | interface WhoAmI { 5 | baseUrl?: string 6 | token: string 7 | } 8 | 9 | export const whoAmI = ({ baseUrl = '', token }: WhoAmI) => async (dispatch: AppDispatch) => { 10 | try { 11 | const response = await fetch(`${baseUrl}/api/auth/whoami`, { 12 | method: 'POST', 13 | headers: { 14 | 'Content-Type': 'application/json', 15 | }, 16 | body: JSON.stringify({ token }), 17 | }) 18 | const user = await response.json() 19 | dispatch(receiveAuthData({ user })) 20 | return user 21 | } catch {} 22 | } 23 | -------------------------------------------------------------------------------- /src/modules/auth/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './withAuthedUser' 2 | -------------------------------------------------------------------------------- /src/modules/auth/utils/withAuthedUser.ts: -------------------------------------------------------------------------------- 1 | import { AppStore, wrapper } from 'modules/app' 2 | import { whoAmI } from 'modules/auth/thunks' 3 | import { absoluteUrl } from 'server-utils' 4 | import { getServerCookies } from 'server-utils/getServerCookies' 5 | 6 | export interface AuthUserOptions { 7 | role: string 8 | } 9 | 10 | export const withAuthedUser = (options?: AuthUserOptions) => (originalGSSP?: any) => 11 | wrapper.getServerSideProps(async (ctx) => { 12 | const { req } = ctx 13 | const store: AppStore = ctx.store 14 | let user 15 | const { token } = getServerCookies(req) 16 | if (token) { 17 | user = await store?.dispatch(whoAmI({ baseUrl: absoluteUrl(req).origin, token })) 18 | } 19 | if (!user) { 20 | // @ts-expect-error `resolvedUrl` is missing in the wrapper typedef. Will be fixed in next release 21 | const resolvedUrl: string = ctx.resolvedUrl ?? '' 22 | return { 23 | redirect: { 24 | destination: `/login?next=${resolvedUrl}`, 25 | permanent: false, 26 | }, 27 | } 28 | } 29 | if (typeof originalGSSP === 'function') { 30 | return originalGSSP({ ctx }) 31 | } 32 | return { 33 | props: { user }, 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /src/modules/example/actions.ts: -------------------------------------------------------------------------------- 1 | import { ExampleSlice } from './slice' 2 | 3 | export const { receiveExample } = ExampleSlice.actions 4 | -------------------------------------------------------------------------------- /src/modules/example/index.ts: -------------------------------------------------------------------------------- 1 | export * from './actions' 2 | export * from './selectors' 3 | export * from './slice' 4 | -------------------------------------------------------------------------------- /src/modules/example/selectors.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from 'modules/app' 2 | import { compose } from 'redux' 3 | 4 | export const selectExample = (state: RootState) => { 5 | return state.example 6 | } 7 | 8 | export const selectSelectExampleMessage = compose((example) => example.message, selectExample) 9 | -------------------------------------------------------------------------------- /src/modules/example/slice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit' 2 | 3 | export interface ExampleType { 4 | message: string 5 | } 6 | 7 | export const initialState: ExampleType = { 8 | message: '', 9 | } 10 | 11 | export const ExampleSlice = createSlice({ 12 | initialState, 13 | name: 'example', 14 | reducers: { 15 | receiveExample: (state, action: PayloadAction) => { 16 | const { message } = action.payload 17 | state.message = message 18 | }, 19 | }, 20 | }) 21 | 22 | export const { reducer } = ExampleSlice 23 | -------------------------------------------------------------------------------- /src/pages/_app.ts: -------------------------------------------------------------------------------- 1 | import { MyApp } from 'components/App' 2 | import 'styles/global.css' 3 | 4 | export default MyApp 5 | -------------------------------------------------------------------------------- /src/pages/api/auth/login.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcryptjs' 2 | import jwt from 'jsonwebtoken' 3 | import { NextApiRequest, NextApiResponse } from 'next' 4 | 5 | import { JWT_EXPIRES, JWT_KEY } from 'settings' 6 | 7 | // Example login handler with hardcoded creds: 8 | // username: "test", password: "password" 9 | // For production you need a real database or a real auth system 10 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 11 | if (req.method === 'POST') { 12 | const { username, password } = req.body 13 | if (username === 'test') { 14 | const match = await bcrypt.compare( 15 | password, 16 | '$2y$10$mj1OMFvVmGAR4gEEXZGtA.R5wYWBZTis72hSXzpxEs.QoXT3ifKSq' // password is "password" 17 | ) 18 | 19 | if (match) { 20 | const user = { 21 | firstName: 'Test', 22 | lastName: 'Smith', 23 | username, 24 | } 25 | 26 | return jwt.sign( 27 | user, 28 | JWT_KEY, 29 | { 30 | expiresIn: JWT_EXPIRES, 31 | }, 32 | (err, token) => { 33 | if (!err && token) { 34 | /* Send success with token */ 35 | return res.status(200).send({ 36 | success: true, 37 | token, 38 | user, 39 | }) 40 | } else { 41 | return res.status(400).send({ error: 'Invalid login credentials' }) 42 | } 43 | } 44 | ) 45 | } else { 46 | return res.status(400).send({ error: 'Invalid login credentials' }) 47 | } 48 | } else { 49 | return res.status(400).send({ error: 'Invalid login credentials' }) 50 | } 51 | } else { 52 | // Handle any other HTTP method 53 | return res.status(404).send('Nothing here') 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/pages/api/auth/whoami.ts: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken' 2 | import { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | import { JWT_KEY } from 'settings' 5 | 6 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 7 | if (req.method === 'POST') { 8 | const { token } = req.body 9 | let user 10 | try { 11 | user = jwt.verify(token, JWT_KEY) 12 | } catch (e) {} 13 | 14 | if (typeof user === 'object') { 15 | return res.status(200).send(user) 16 | } 17 | return res.status(404).send('Nothing here') 18 | } 19 | return res.status(404).send('Nothing here') 20 | } 21 | -------------------------------------------------------------------------------- /src/pages/example.ts: -------------------------------------------------------------------------------- 1 | import { Example, getServerSideProps } from 'components/Example' 2 | 3 | export default Example 4 | export { getServerSideProps } 5 | -------------------------------------------------------------------------------- /src/pages/index.ts: -------------------------------------------------------------------------------- 1 | import { Home } from 'components/Home' 2 | 3 | export default Home 4 | -------------------------------------------------------------------------------- /src/pages/login.ts: -------------------------------------------------------------------------------- 1 | import { Login } from 'components/Login' 2 | 3 | export default Login 4 | -------------------------------------------------------------------------------- /src/pages/private-ssp.ts: -------------------------------------------------------------------------------- 1 | import { PrivateSSP, getServerSideProps as ssp } from 'components/Private' 2 | import { withAuthedUser } from 'modules/auth' 3 | 4 | export default PrivateSSP 5 | 6 | export const getServerSideProps = withAuthedUser({ role: 'admin' })(ssp) 7 | -------------------------------------------------------------------------------- /src/pages/private.ts: -------------------------------------------------------------------------------- 1 | import { Private } from 'components/Private' 2 | import { withAuthedUser } from 'modules/auth' 3 | 4 | export default Private 5 | 6 | // withAuthedUser makes the page private, redirects to login when user is not authed 7 | export const getServerSideProps = withAuthedUser()() 8 | -------------------------------------------------------------------------------- /src/server-utils/absoluteUrl.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage } from 'http' 2 | 3 | import { NEXT_PUBLIC_APP_PROTOCOL } from 'settings' 4 | 5 | export function absoluteUrl(req?: IncomingMessage, localhostAddress = 'localhost:3001') { 6 | let host = (req?.headers ? req.headers.host : window.location.host) ?? localhostAddress 7 | const protocol = NEXT_PUBLIC_APP_PROTOCOL 8 | 9 | if (req?.headers['x-forwarded-host'] && typeof req.headers['x-forwarded-host'] === 'string') { 10 | host = req.headers['x-forwarded-host'] 11 | } 12 | 13 | return { 14 | host, 15 | origin: `${protocol}//${host}`, 16 | protocol, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/server-utils/getServerCookies.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage } from 'http' 2 | 3 | export const getServerCookies = (req: IncomingMessage) => { 4 | const cookies: { [key: string]: string } = {} 5 | if (req.headers?.cookie) { 6 | const cookiesItems = req.headers.cookie.split('; ') 7 | cookiesItems.forEach((cookie: string) => { 8 | const parsedItem = cookie.split('=') 9 | if (parsedItem.length === 2) { 10 | cookies[parsedItem[0]] = decodeURI(parsedItem[1]) 11 | } 12 | }) 13 | } 14 | return cookies 15 | } 16 | -------------------------------------------------------------------------------- /src/server-utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './absoluteUrl' 2 | -------------------------------------------------------------------------------- /src/settings/index.ts: -------------------------------------------------------------------------------- 1 | // private settings 2 | export const JWT_EXPIRES = 31556926 // 1 year in seconds 3 | export const JWT_KEY = process.env.JWT_KEY ?? '' 4 | 5 | // public settings 6 | export const NEXT_PUBLIC_APP_PROTOCOL = process.env.NEXT_PUBLIC_APP_PROTOCOL ?? 'http:' 7 | export const NEXT_PUBLIC_NODE_ENV = process.env.NEXT_PUBLIC_NODE_ENV ?? 'development' 8 | -------------------------------------------------------------------------------- /src/styles/global.css: -------------------------------------------------------------------------------- 1 | *, *::before, *::after { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif; 7 | font-size: 16px; 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { isServer } from './isServer' 2 | -------------------------------------------------------------------------------- /src/utils/isServer.ts: -------------------------------------------------------------------------------- 1 | export const isServer = () => typeof window === 'undefined' 2 | -------------------------------------------------------------------------------- /test/components/Home.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { Home } from 'components/Home' 4 | import { renderWithRedux } from '../renderWithRedux' 5 | 6 | describe('Home', () => { 7 | it('renders', () => { 8 | const { getByText } = renderWithRedux(, { initialState: {} }) 9 | expect(getByText('Next.js Starter project')).toBeInTheDocument() 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /test/renderWithRedux.tsx: -------------------------------------------------------------------------------- 1 | import configureMockStore from 'redux-mock-store' 2 | import { render } from '@testing-library/react' 3 | import thunk from 'redux-thunk' 4 | import React from 'react' 5 | import { Provider } from 'react-redux' 6 | 7 | const middlewares = [thunk] 8 | const mockStore = configureMockStore(middlewares) 9 | 10 | interface Options { 11 | initialState: any 12 | } 13 | 14 | export const renderWithRedux = (children: React.ReactNode, { initialState }: Options) => { 15 | const store = mockStore(initialState) 16 | return render({children}) 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "baseUrl": "src", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "isolatedModules": true, 8 | "jsx": "preserve", 9 | "lib": [ 10 | "dom", 11 | "dom.iterable", 12 | "esnext" 13 | ], 14 | "module": "esnext", 15 | "moduleResolution": "node", 16 | "noEmit": true, 17 | "noImplicitAny": true, 18 | "resolveJsonModule": true, 19 | "skipLibCheck": true, 20 | "strict": true, 21 | "target": "es5" 22 | }, 23 | "exclude": [ 24 | "node_modules" 25 | ], 26 | "include": [ 27 | "next-env.d.ts", 28 | "**/*.ts", 29 | "**/*.tsx" 30 | ] 31 | } 32 | --------------------------------------------------------------------------------