├── .env.local ├── .eslintrc.json ├── app ├── favicon.ico ├── dashboard │ ├── layout.tsx │ └── page.tsx ├── auth │ ├── facebook │ │ └── page.tsx │ ├── google │ │ └── page.tsx │ ├── login │ │ └── page.tsx │ └── register │ │ └── page.tsx ├── layout.tsx ├── password-reset │ ├── page.tsx │ └── [uid] │ │ └── [token] │ │ └── page.tsx ├── not-found.tsx ├── activation │ └── [uid] │ │ └── [token] │ │ └── page.tsx └── page.tsx ├── styles └── globals.css ├── next.config.js ├── postcss.config.js ├── components ├── utils │ ├── index.ts │ ├── Setup.tsx │ └── RequireAuth.tsx ├── forms │ ├── index.ts │ ├── PasswordResetForm.tsx │ ├── LoginForm.tsx │ ├── PasswordResetConfirmForm.tsx │ ├── RegisterForm.tsx │ ├── Input.tsx │ └── Form.tsx └── common │ ├── index.ts │ ├── Footer.tsx │ ├── Spinner.tsx │ ├── SocialButton.tsx │ ├── SocialButtons.tsx │ ├── List.tsx │ ├── NavLink.tsx │ └── Navbar.tsx ├── utils ├── index.ts └── continue-with-social-auth.ts ├── redux ├── provider.tsx ├── hooks.ts ├── store.ts ├── features │ ├── authSlice.ts │ └── authApiSlice.ts └── services │ └── apiSlice.ts ├── hooks ├── index.ts ├── use-verify.ts ├── use-reset-password.ts ├── use-social-auth.ts ├── use-login.ts ├── use-reset-password-confirm.ts └── use-register.ts ├── README.md ├── .gitignore ├── tailwind.config.js ├── public ├── vercel.svg └── next.svg ├── tsconfig.json └── package.json /.env.local: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_HOST=http://localhost:8000 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkedweb/full-auth/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /components/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { default as RequireAuth } from './RequireAuth'; 2 | export { default as Setup } from './Setup'; 3 | -------------------------------------------------------------------------------- /app/dashboard/layout.tsx: -------------------------------------------------------------------------------- 1 | import { RequireAuth } from '@/components/utils'; 2 | 3 | interface Props { 4 | children: React.ReactNode; 5 | } 6 | 7 | export default function Layout({ children }: Props) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /utils/index.ts: -------------------------------------------------------------------------------- 1 | import continueWithSocialAuth from './continue-with-social-auth'; 2 | 3 | export const continueWithGoogle = () => 4 | continueWithSocialAuth('google-oauth2', 'google'); 5 | export const continueWithFacebook = () => 6 | continueWithSocialAuth('facebook', 'facebook'); 7 | -------------------------------------------------------------------------------- /components/utils/Setup.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useVerify } from '@/hooks'; 4 | import { ToastContainer } from 'react-toastify'; 5 | import 'react-toastify/dist/ReactToastify.css'; 6 | 7 | export default function Setup() { 8 | useVerify(); 9 | 10 | return ; 11 | } 12 | -------------------------------------------------------------------------------- /redux/provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { store } from './store'; 4 | import { Provider } from 'react-redux'; 5 | 6 | interface Props { 7 | children: React.ReactNode; 8 | } 9 | 10 | export default function CustomProvider({ children }: Props) { 11 | return {children}; 12 | } 13 | -------------------------------------------------------------------------------- /redux/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useSelector, useDispatch } from 'react-redux'; 2 | import type { TypedUseSelectorHook } from 'react-redux'; 3 | import type { RootState, AppDispatch } from './store'; 4 | 5 | export const useAppDispatch: () => AppDispatch = useDispatch; 6 | export const useAppSelector: TypedUseSelectorHook = useSelector; 7 | -------------------------------------------------------------------------------- /components/forms/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Form } from './Form'; 2 | export { default as Input } from './Input'; 3 | export { default as LoginForm } from './LoginForm'; 4 | export { default as PasswordResetConfirmForm } from './PasswordResetConfirmForm'; 5 | export { default as PasswordResetForm } from './PasswordResetForm'; 6 | export { default as RegisterForm } from './RegisterForm'; 7 | -------------------------------------------------------------------------------- /components/common/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Footer } from './Footer'; 2 | export { default as Navbar } from './Navbar'; 3 | export { default as List } from './List'; 4 | export { default as NavLink } from './NavLink'; 5 | export { default as SocialButton } from './SocialButton'; 6 | export { default as SocialButtons } from './SocialButtons'; 7 | export { default as Spinner } from './Spinner'; 8 | -------------------------------------------------------------------------------- /hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useLogin } from './use-login'; 2 | export { default as useRegister } from './use-register'; 3 | export { default as useResetPasswordConfirm } from './use-reset-password-confirm'; 4 | export { default as useResetPassword } from './use-reset-password'; 5 | export { default as useSocialAuth } from './use-social-auth'; 6 | export { default as useVerify } from './use-verify'; 7 | -------------------------------------------------------------------------------- /components/common/Footer.tsx: -------------------------------------------------------------------------------- 1 | export default function Footer() { 2 | return ( 3 |
4 |
5 |
6 |

7 | © 2023 Full Auth, Inc. All rights reserved. 8 |

9 |
10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Full Auth 2 | 3 | To run the development server: 4 | 5 | ```bash 6 | npm run dev 7 | # or 8 | yarn dev 9 | # or 10 | pnpm dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # vercel 28 | .vercel 29 | 30 | # typescript 31 | *.tsbuildinfo 32 | next-env.d.ts 33 | -------------------------------------------------------------------------------- /app/auth/facebook/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useSocialAuthenticateMutation } from '@/redux/features/authApiSlice'; 4 | import { useSocialAuth } from '@/hooks'; 5 | import { Spinner } from '@/components/common'; 6 | 7 | export default function Page() { 8 | const [facebookAuthenticate] = useSocialAuthenticateMutation(); 9 | useSocialAuth(facebookAuthenticate, 'facebook'); 10 | 11 | return ( 12 |
13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /app/auth/google/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useSocialAuthenticateMutation } from '@/redux/features/authApiSlice'; 4 | import { useSocialAuth } from '@/hooks'; 5 | import { Spinner } from '@/components/common'; 6 | 7 | export default function Page() { 8 | const [googleAuthenticate] = useSocialAuthenticateMutation(); 9 | useSocialAuth(googleAuthenticate, 'google-oauth2'); 10 | 11 | return ( 12 |
13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 5 | './components/**/*.{js,ts,jsx,tsx,mdx}', 6 | './app/**/*.{js,ts,jsx,tsx,mdx}', 7 | ], 8 | theme: { 9 | extend: { 10 | backgroundImage: { 11 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 12 | 'gradient-conic': 13 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 14 | }, 15 | }, 16 | }, 17 | plugins: [require('@tailwindcss/forms')], 18 | }; 19 | -------------------------------------------------------------------------------- /components/common/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'classnames'; 2 | import { ImSpinner3 } from 'react-icons/im'; 3 | 4 | interface Props { 5 | sm?: boolean; 6 | md?: boolean; 7 | lg?: boolean; 8 | } 9 | 10 | export default function Spinner({ sm, md, lg }: Props) { 11 | const className = cn('animate-spin text-white-300 fill-white-300 mr-2', { 12 | 'w-4 h-4': sm, 13 | 'w-6 h-6': md, 14 | 'w-8 h-8': lg, 15 | }); 16 | 17 | return ( 18 |
19 | 20 | Loading... 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /redux/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import { apiSlice } from './services/apiSlice'; 3 | import authReducer from './features/authSlice'; 4 | 5 | export const store = configureStore({ 6 | reducer: { 7 | [apiSlice.reducerPath]: apiSlice.reducer, 8 | auth: authReducer, 9 | }, 10 | middleware: getDefaultMiddleware => 11 | getDefaultMiddleware().concat(apiSlice.middleware), 12 | devTools: process.env.NODE_ENV !== 'production', 13 | }); 14 | 15 | export type RootState = ReturnType<(typeof store)['getState']>; 16 | export type AppDispatch = (typeof store)['dispatch']; 17 | -------------------------------------------------------------------------------- /hooks/use-verify.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useAppDispatch } from '@/redux/hooks'; 3 | import { setAuth, finishInitialLoad } from '@/redux/features/authSlice'; 4 | import { useVerifyMutation } from '@/redux/features/authApiSlice'; 5 | 6 | export default function useVerify() { 7 | const dispatch = useAppDispatch(); 8 | 9 | const [verify] = useVerifyMutation(); 10 | 11 | useEffect(() => { 12 | verify(undefined) 13 | .unwrap() 14 | .then(() => { 15 | dispatch(setAuth()); 16 | }) 17 | .finally(() => { 18 | dispatch(finishInitialLoad()); 19 | }); 20 | }, []); 21 | } 22 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/utils/RequireAuth.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { redirect } from 'next/navigation'; 4 | import { useAppSelector } from '@/redux/hooks'; 5 | import { Spinner } from '@/components/common'; 6 | 7 | interface Props { 8 | children: React.ReactNode; 9 | } 10 | 11 | export default function RequireAuth({ children }: Props) { 12 | const { isLoading, isAuthenticated } = useAppSelector(state => state.auth); 13 | 14 | if (isLoading) { 15 | return ( 16 |
17 | 18 |
19 | ); 20 | } 21 | 22 | if (!isAuthenticated) { 23 | redirect('/auth/login'); 24 | } 25 | 26 | return <>{children}; 27 | } 28 | -------------------------------------------------------------------------------- /components/forms/PasswordResetForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useResetPassword } from '@/hooks'; 4 | import { Form } from '@/components/forms'; 5 | 6 | export default function PasswordResetForm() { 7 | const { email, isLoading, onChange, onSubmit } = useResetPassword(); 8 | 9 | const config = [ 10 | { 11 | labelText: 'Email address', 12 | labelId: 'email', 13 | type: 'email', 14 | onChange, 15 | value: email, 16 | required: true, 17 | }, 18 | ]; 19 | 20 | return ( 21 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /components/common/SocialButton.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'classnames'; 2 | 3 | interface Props { 4 | provider: 'google' | 'facebook'; 5 | children: React.ReactNode; 6 | [rest: string]: any; 7 | } 8 | 9 | export default function SocialButton({ provider, children, ...rest }: Props) { 10 | const className = cn( 11 | 'flex-1 text-white rounded-md px-3 mt-3 py-2 font-medium', 12 | { 13 | 'bg-red-500 hover:bg-red-400': provider === 'google', 14 | 'bg-blue-500 hover:bg-blue-400': provider === 'facebook', 15 | } 16 | ); 17 | 18 | return ( 19 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /components/common/SocialButtons.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ImGoogle, ImFacebook } from 'react-icons/im'; 4 | import { SocialButton } from '@/components/common'; 5 | import { continueWithGoogle, continueWithFacebook } from '@/utils'; 6 | 7 | export default function SocialButtons() { 8 | return ( 9 |
10 | 11 | Google Signin 12 | 13 | 14 | Facebook Signin 15 | 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /redux/features/authSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | interface AuthState { 4 | isAuthenticated: boolean; 5 | isLoading: boolean; 6 | } 7 | 8 | const initialState = { 9 | isAuthenticated: false, 10 | isLoading: true, 11 | } as AuthState; 12 | 13 | const authSlice = createSlice({ 14 | name: 'auth', 15 | initialState, 16 | reducers: { 17 | setAuth: state => { 18 | state.isAuthenticated = true; 19 | }, 20 | logout: state => { 21 | state.isAuthenticated = false; 22 | }, 23 | finishInitialLoad: state => { 24 | state.isLoading = false; 25 | }, 26 | }, 27 | }); 28 | 29 | export const { setAuth, logout, finishInitialLoad } = authSlice.actions; 30 | export default authSlice.reducer; 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /components/common/List.tsx: -------------------------------------------------------------------------------- 1 | import { Spinner } from '@/components/common'; 2 | 3 | interface Config { 4 | label: string; 5 | value: string | undefined; 6 | } 7 | 8 | interface Props { 9 | config: Config[]; 10 | } 11 | 12 | export default function List({ config }: Props) { 13 | return ( 14 |
    15 | {config.map(({ label, value }) => ( 16 |
  • 17 |
    18 |

    19 | {label} 20 |

    21 |
    22 |
    23 |

    24 | {value || } 25 |

    26 |
    27 |
  • 28 | ))} 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /components/forms/LoginForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useLogin } from '@/hooks'; 4 | import { Form } from '@/components/forms'; 5 | 6 | export default function LoginForm() { 7 | const { email, password, isLoading, onChange, onSubmit } = useLogin(); 8 | 9 | const config = [ 10 | { 11 | labelText: 'Email address', 12 | labelId: 'email', 13 | type: 'email', 14 | value: email, 15 | required: true, 16 | }, 17 | { 18 | labelText: 'Password', 19 | labelId: 'password', 20 | type: 'password', 21 | value: password, 22 | link: { 23 | linkText: 'Forgot password?', 24 | linkUrl: '/password-reset', 25 | }, 26 | required: true, 27 | }, 28 | ]; 29 | 30 | return ( 31 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "full-auth", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@headlessui/react": "^1.7.14", 13 | "@heroicons/react": "^2.0.18", 14 | "@reduxjs/toolkit": "^1.9.5", 15 | "@tailwindcss/forms": "^0.5.3", 16 | "@types/node": "20.2.3", 17 | "@types/react": "18.2.6", 18 | "@types/react-dom": "18.2.4", 19 | "async-mutex": "^0.4.0", 20 | "autoprefixer": "10.4.14", 21 | "classnames": "^2.3.2", 22 | "eslint": "8.41.0", 23 | "eslint-config-next": "13.4.3", 24 | "next": "13.4.4", 25 | "postcss": "8.4.23", 26 | "react": "18.2.0", 27 | "react-dom": "18.2.0", 28 | "react-icons": "^4.8.0", 29 | "react-redux": "^8.0.5", 30 | "react-toastify": "^9.1.3", 31 | "tailwindcss": "3.3.2", 32 | "typescript": "5.0.4" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import '@/styles/globals.css'; 2 | import { Inter } from 'next/font/google'; 3 | import type { Metadata } from 'next'; 4 | import Provider from '@/redux/provider'; 5 | import { Footer, Navbar } from '@/components/common'; 6 | import { Setup } from '@/components/utils'; 7 | 8 | const inter = Inter({ subsets: ['latin'] }); 9 | 10 | export const metadata: Metadata = { 11 | title: 'Full Auth', 12 | description: 'Full Auth application that provides jwt authentication', 13 | }; 14 | 15 | export default function RootLayout({ 16 | children, 17 | }: { 18 | children: React.ReactNode; 19 | }) { 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 |
27 | {children} 28 |
29 |