Skip to content

Commit

Permalink
➡️ merge pull request #48 from devsoc-unsw/feature/login-state
Browse files Browse the repository at this point in the history
login is stored as state, profile page only accessible when logged in, add protectedroute component
  • Loading branch information
lachlanshoesmith authored Dec 13, 2024
2 parents e87e336 + 0f4268b commit 1fa63c5
Show file tree
Hide file tree
Showing 11 changed files with 226 additions and 53 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
```bash
SESSION_SECRET=
NODE_ENV=development
ALLOWED_ORIGINS=commaseparated,regexes,slashesnotrequired
ALLOWED_ORIGINS=commaseparated,urls,or,regexes
DATABASE_URL="postgresql://.../postgres?pgbouncer=true"
DIRECT_URL="postgresql://.../postgres"
REDIS_PORT=6379
Expand Down
24 changes: 22 additions & 2 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,26 @@ declare module "express-session" {
}

// Initialize client.
if (process.env["REDIS_PORT"] === undefined) {
if (
process.env["REDIS_PORT"] === undefined ||
process.env["REDIS_PORT"] === ""
) {
console.log(process.env);
console.error("Redis port not provided in .env file");
process.exit(1);
}

let allowed_origins;
if (
process.env["ALLOWED_ORIGINS"] === undefined ||
process.env["ALLOWED_ORIGINS"] === ""
) {
console.log("Warning: ALLOWED_ORIGINS not specified. Using wildcard *.");
allowed_origins = ["*"];
} else {
allowed_origins = process.env["ALLOWED_ORIGINS"]?.split(",");
}

let redisClient = createClient({
url: `redis://localhost:${process.env["REDIS_PORT"]}`,
});
Expand All @@ -56,7 +71,12 @@ const app = express();
const SERVER_PORT = 5180;
const SALT_ROUNDS = 10;

app.use(cors());
app.use(
cors({
origin: allowed_origins,
credentials: true,
})
);
app.use(express.json());

if (process.env["SESSION_SECRET"] === undefined) {
Expand Down
90 changes: 73 additions & 17 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import "./App.css";
import NavBar from "./NavBar/NavBar";
import { BrowserRouter, Route, Routes } from "react-router";
import { BrowserRouter, Navigate, Route, Routes } from "react-router";
import HomePage from "./HomePage/HomePage";
import AboutPage from "./About/About";
import Calendar from "./Calendar/Calendar";
Expand All @@ -11,26 +11,82 @@ import { ProfilePage } from "./Settings/SettingsPage/ProfilePage/ProfilePage";
import { EventManagementPage } from "./Settings/SettingsPage/EventManagementPage/EventManagementPage";
import { CreateNewEventPage } from "./Settings/SettingsPage/EventManagementPage/CreateNewEvent/CreateNewEvent";
import { DiscordPage } from "./Settings/SettingsPage/DiscordPage/DiscordPage";
import { Unauthenticated } from "./Unauthenticated/Unauthenticated";
import { ProtectedRoute } from "./ProtectedRoute/ProtectedRoute";
import { useEffect, useState } from "react";
import { User, UserContext } from "./UserContext/UserContext";

function App() {
const [user, setUser] = useState<User | null>(null);

useEffect(() => {
fetch("http://localhost:5180/user", {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
}).then((res) => {
if (res.ok) {
res.json().then((data) => {
setUser(data);
});
}
});
}, []);

return (
<BrowserRouter>
<div className="page">
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/timeline" element={<Calendar />} /> //
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/settings" element={<Settings />}>
<Route path="profile" element={<ProfilePage />} />
<Route path="events" element={<EventManagementPage />} />
<Route path="events/new" element={<CreateNewEventPage />} />
<Route path="discord" element={<DiscordPage />} />
</Route>
</Routes>
</div>
<NavBar profileImage="https://i.redd.it/white-pharaoh-in-school-textbook-v0-fgr8oliazlkd1.jpg?width=225&format=pjpg&auto=webp&s=04dc4c2c8a0170c4e161091673352cd966591475"></NavBar>
<UserContext.Provider value={{ user, setUser }}>
<div className="page">
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/timeline" element={<Calendar />} /> //
<Route
path="/login"
element={
<ProtectedRoute
isAuthenticated={user === null}
fallback={<Navigate to="/settings/profile" />}
>
<LoginPage />
</ProtectedRoute>
}
/>
<Route
path="/register"
element={
<ProtectedRoute
isAuthenticated={user === null}
fallback={<Navigate to="/settings/profile" />}
>
<RegisterPage />
</ProtectedRoute>
}
/>
<Route
path="/settings"
element={
// this propagates to all child routes
<ProtectedRoute
isAuthenticated={user !== null && user.id !== undefined}
fallback={<Navigate to="/login" />}
>
<Settings />
</ProtectedRoute>
}
>
<Route path="profile" element={<ProfilePage />} />
<Route path="events" element={<EventManagementPage />} />
<Route path="events/new" element={<CreateNewEventPage />} />
<Route path="discord" element={<DiscordPage />} />
</Route>
<Route path="/unauthenticated" element={<Unauthenticated />} />
</Routes>
</div>
<NavBar profileImage="https://i.redd.it/white-pharaoh-in-school-textbook-v0-fgr8oliazlkd1.jpg?width=225&format=pjpg&auto=webp&s=04dc4c2c8a0170c4e161091673352cd966591475"></NavBar>
</UserContext.Provider>
</BrowserRouter>
);
}
Expand Down
60 changes: 34 additions & 26 deletions frontend/src/AuthScreen/AuthScreen.module.css
Original file line number Diff line number Diff line change
@@ -1,37 +1,45 @@
.container{
width: 20%;
height: 50%;
background-color: hsl(0, 0%, 100%);
border-radius: 10px;
margin: auto;
padding: 30px;
min-width: 500px;
min-height: 300px;
.container {
width: 20%;
height: 50%;
background-color: hsl(0, 0%, 100%);
border-radius: 10px;
margin: auto;
padding: 30px;
min-width: 500px;
min-height: 300px;
}

.form{
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
.form {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}

.headerText{
padding-top: 34px;
padding-bottom: 46px;
.headerText {
padding-top: 34px;
padding-bottom: 46px;
}

.footer{
text-align: center;
color: hsl(0, 0%, 62%);
.footer {
text-align: center;
color: hsl(0, 0%, 62%);
}

.footer div:first-child {
padding-top: 50px;
padding-top: 50px;
}

.error{
padding: 10px;
color: hsl(0, 100%, 64%);
font-weight: bold;
}
.error,
.success {
padding: 10px;
font-weight: bold;
}

.error {
color: hsl(0, 100%, 64%);
}

.success {
color: hsl(119, 100%, 30%);
}
12 changes: 10 additions & 2 deletions frontend/src/AuthScreen/AuthScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ReactNode, FormEvent } from "react";
import { ReactNode, FormEvent, Fragment } from "react";
import classes from "./AuthScreen.module.css";
import Button from "../Button/Button";
import { AuthError } from "../errorHandler";
Expand All @@ -12,6 +12,7 @@ type AuthScreenProp = {
footer?: ReactNode;
onSubmit: (event: FormEvent<HTMLFormElement>) => void;
error?: AuthError;
success?: string;
};

export function AuthScreen(props: AuthScreenProp) {
Expand All @@ -23,7 +24,9 @@ export function AuthScreen(props: AuthScreenProp) {
</header>
<main>
<form className={classes.form} onSubmit={props.onSubmit}>
{props.inputs.map((input: ReactNode) => input)}
{props.inputs.map((input: ReactNode, index) => (
<Fragment key={index}>{input}</Fragment>
))}
<Button
variant={ButtonVariants.Primary}
type="submit"
Expand All @@ -36,6 +39,11 @@ export function AuthScreen(props: AuthScreenProp) {

<footer className={classes.footer}>
{props.footer && <div>{props.footer}</div>}{" "}
{props.success && (
<div className={classes.success}>
<p>{props.success}</p>
</div>
)}
{props.error && (
<div>
<p className={classes.error}>{props.error.message}</p>
Expand Down
20 changes: 17 additions & 3 deletions frontend/src/Login/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,27 @@ import { AuthScreen } from "../AuthScreen/AuthScreen";
import { TextInput, TextOptions } from "../TextInput/TextInput";
import { UserCircleIcon } from "@heroicons/react/24/outline";
import { LockClosedIcon } from "@heroicons/react/24/outline";
import { useState, FormEvent } from "react";
import { Link } from "react-router";
import { useState, FormEvent, useContext } from "react";
import { Link, useNavigate } from "react-router";
import { errorHandler, AuthError } from "../errorHandler";
import { UserContext, User } from "../UserContext/UserContext";

export default function LoginPage() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState<AuthError | undefined>(undefined);
const [success, setSuccess] = useState<string | undefined>(undefined);
const navigate = useNavigate();
const { setUser } = useContext(UserContext);

async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const res = await fetch("http://localhost:5180/auth/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify({
username,
password,
Expand All @@ -27,8 +33,15 @@ export default function LoginPage() {

if (!res.ok) {
setError(errorHandler(json.error));
} else {
} else if (setUser) {
setError(undefined);
setUser(json as User);
setSuccess("Logged in successfully! Redirecting...");
setTimeout(() => {
navigate("/timeline");
}, 1000);
} else {
setError(errorHandler("Couldn't update user object."));
}
}

Expand Down Expand Up @@ -67,6 +80,7 @@ export default function LoginPage() {
footer={<p>Forgot Password</p>}
onSubmit={handleSubmit}
error={error}
success={success}
/>
<div className={classes.lower} />
</main>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/NavBar/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function NavBar(props: NavBarProps) {
className={({ isActive }) =>
isActive && view === "calendar" ? classes.active : ""
}
to="/timeline?view=calendar"
to="/timeline?view=calendar"
>
Calendar
</NavLink>
Expand All @@ -52,7 +52,7 @@ function NavBar(props: NavBarProps) {
style={{
backgroundImage: `url(${props.profileImage})`,
}}
to="/login"
to="/settings"
></NavLink>
</nav>
);
Expand Down
20 changes: 20 additions & 0 deletions frontend/src/ProtectedRoute/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ReactNode } from "react";
import { Navigate } from "react-router";

interface ProtectedRouteProps {
isAuthenticated: boolean;
children: ReactNode;
fallback?: ReactNode;
}

export const ProtectedRoute = (props: ProtectedRouteProps) => {
if (!props.isAuthenticated) {
if (props.fallback) {
return props.fallback;
} else {
return <Navigate to="/login" />;
}
}

return props.children;
};
10 changes: 10 additions & 0 deletions frontend/src/Unauthenticated/Unauthenticated.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.container {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}

.container > article {
line-height: 2;
}
18 changes: 18 additions & 0 deletions frontend/src/Unauthenticated/Unauthenticated.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Link } from "react-router";
import classes from "./Unauthenticated.module.css";

export const Unauthenticated = () => {
return (
<main className={classes.container}>
<article>
<p>Sorry, you don't permission to view this page.</p>
<p>
Are you <Link to="/login">logged in?</Link>
</p>
<p>
<Link to="/">Home</Link>
</p>
</article>
</main>
);
};
Loading

0 comments on commit 1fa63c5

Please sign in to comment.