본문 바로가기

트위터(React, TypeScript, Firebase, Vite)

09. Authentication - Protected Routes

protected-route.tsx

import React from "react";
import { auth } from "./firebase";
import { Navigate } from "react-router-dom";

export default function ProtectedRoute({children} : {
    children : React.ReactNode;
}){

    const user = auth.currentUser;
    if(user === null){
        return <Navigate to="/login" />
    }
    return children;
}

 

App.tsx

import { useEffect, useState } from 'react'
import { createBrowserRouter, RouterProvider} from 'react-router-dom';
import Layout from './components/layout';
import Home from './routes/home';
import Profile from './routes/profile';
import Login from './routes/login';
import CreateAccount from './routes/create-account';
import { createGlobalStyle } from 'styled-components';
import reset from "styled-reset";
import LoadingScreen from './components/loading-screen';

import { auth } from './routes/firebase';
import ProtectedRoute from './routes/protected-route';

const router = createBrowserRouter([
  {
    path:"/",
    element:
    /*
      <Layout />,
    */
    <ProtectedRoute>
      <Layout />
    </ProtectedRoute>,
    children: [
      {
        path: "",
        element: 
       /*  
        <ProtectedRoute>
          <Home />
        </ProtectedRoute> 
        */
        <Home />
      },
      {
        path: "profile",
        element:
        /* 
        <ProtectedRoute>
          <Profile />
        </ProtectedRoute>
        */
        <Profile />
      }
    ]
  },
  {
    path: "/login",
    element: <Login />
  },
  {
    path:"/create-account",
    element: <CreateAccount />
  }
]);

const GlobalStyles = createGlobalStyle`
   ${reset};
   * {
     box-sizing: border-box;
   }
   body {
     background-color: black;
     color:white;
     font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
   }
 `;

function App() {

  const [isLoading, setLoading] = useState(true);
  const init = async() => {
    
    await auth.authStateReady();
              // Firebase가 쿠키와 토큰을 읽고 백엔드와 소통해서 로그인여부를 확인하는 동안 기다림
    setLoading(false);
  };

  useEffect(()=>{
    init();
  },[]);
   
  return <>
    <GlobalStyles />
    {isLoading ? <LoadingScreen /> : <RouterProvider router = {router} />}
  </>;
}

export default App

 

layout.tsx

import { Outlet } from "react-router-dom";

export default function Layout(){
    return(
        <>
            <Outlet />
        </>
    )
}

 

home.tsx

import styled from "styled-components";
import { auth } from "./firebase";

export default function Home(){
    const logOut = () => {
        auth.signOut();
    };
    return <h1>
                <button onClick={logOut}>Log Out</button>
           </h1>
}

 

profile.tsx

export default function Profile(){
    return <h1>Profile</h1>
}

 

login.tsx

 

export default function Login(){
    return <h1>login</h1>;
}

 

create-account.tsx

import { createUserWithEmailAndPassword, updateProfile } from "firebase/auth";
import { useState } from "react";
import { styled } from "styled-components";
import { auth } from "./firebase";
import { useNavigate } from "react-router-dom";

const Wrapper = styled.div`
  height: 100vh;                          /* 화면 전체 높이를 기준으로 중앙 정렬 */ 
  display: flex;
  justify-content: center;                /* 수평 중앙 */
  align-items: center;                    /* 수직 중앙 */
  flex-direction: column;
  width: 420px;
  margin: 0 auto;                         /* 화면 가운데로 */
  padding: 50px 0px;

`;

const Title = styled.h1`
  font-size: 42px;
`;

const Form = styled.form`
  margin-top: 50px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  width: 100%;
`;

const Input = styled.input`
  padding: 10px 20px;
  border-radius: 50px;
  border: none;
  width: 100%;
  font-size: 16px;
  &[type="submit"] {
    cursor: pointer;
    &:hover {
      opacity: 0.8;
    }
  }
`;

const Error = styled.span`
  font-weight: 600;
  color: tomato;
`;

export default function CreateAccount() {
  const navigate = useNavigate();
  const [isLoading, setLoading] = useState(false);
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState("");
  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const {
      target: { name, value },
    } = e;
    if (name === "name") {
      setName(value);
    } else if (name === "email") {
      setEmail(value);
    } else if (name === "password") {
      setPassword(value);
    }
  };
  const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    if(isLoading === true ||name === "" || email === "" || password === ""){
      return;
    }
    try {
      setLoading(true);
      const credentials = await createUserWithEmailAndPassword(auth, email, password)
                                                              // createUserWithEmailAndPassword는 Firebase Auth SDK에서 제공하는 함수로 인자에 auth 인스턴스가 필요하다.
                                                              // firebase.ts 파일 내 auth 인스턴스가 생성되어 있음!
      await updateProfile(credentials.user, {displayName : name});  
      navigate("/");                                                            
    } catch (e) {
     
    } finally {
      setLoading(false);
    }
  };
  return (
    <Wrapper>
      <Title>Join 𝕏</Title>
      <Form onSubmit={onSubmit}>
        <Input
          onChange={onChange}
          name="name"
          value={name}
          placeholder="Name"
          type="text"
          required
        />
        <Input
          onChange={onChange}
          name="email"
          value={email}
          placeholder="Email"
          type="email"
          required
        />
        <Input
          onChange={onChange}
          value={password}
          name="password"
          placeholder="Password"
          type="password"
          required
        />
        <Input
          type="submit"
          value={isLoading ? "Loading..." : "Create Account"}
        />
      </Form>
      {error !== "" ? <Error>{error}</Error> : null}
    </Wrapper>
  );
}

 

 

 

 

 

 

 

 

 

 

nwitter-reloaded.z01
19.53MB
nwitter-reloaded.z02
19.53MB
nwitter-reloaded.z03
19.53MB
nwitter-reloaded.z04
19.53MB
nwitter-reloaded.z05
19.53MB
nwitter-reloaded.zip
14.58MB