Skip to content

Commit

Permalink
Implemented Github OAuth feature (#158)
Browse files Browse the repository at this point in the history
* implemented github oauth feature

* ran make format and rebased from master branch

* removed unused dependency fastapi-sessions

* CONTRIBUTING.md documentation for Github OAuth configuration and replacing Button with TCButton in Login and RegistrationEmail components

* Multi-line for condition expression in add_user function

* decrease line width for api calls in test

* Add some helpful conditioning/tips for OAuth exclusive users

* fixed most of static checks

* fixed all static check errors

---------

Co-authored-by: shaunchua <[email protected]>
Co-authored-by: Dennis Chen <[email protected]>
  • Loading branch information
3 people authored Jul 11, 2024
1 parent 1276119 commit a7f467c
Show file tree
Hide file tree
Showing 19 changed files with 351 additions and 59 deletions.
11 changes: 11 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,19 @@ export ROBOLIST_SMTP_SENDER_NAME=
export ROBOLIST_SMTP_USERNAME=
export ROBOLIST_REDIS_HOST=
export ROBOLIST_REDIS_PASSWORD=
export GITHUB_CLIENT_ID=
export GITHUB_CLIENT_SECRET=
```

### Github OAuth Configuration

To run Github OAuth locally, you must follow these steps:
1. Create an OAuth App on [Github Developer Settings](https://github.com/settings/developers)
2. Set both Homepage URL and Authorization callback URL to `http://127.0.0.1:3000` before you `Update application` on Github Oauth App configuration
3. Copy the Client ID and Client Secret from Github OAuth App configuration and set them in `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` respectively
4. Run `source env.sh` in your Fast API terminal window to ensure it has access to the github environment variables


## React

To install the React dependencies, use [nvm](https://github.com/nvm-sh/nvm) and [npm](https://www.npmjs.com/):
Expand Down
30 changes: 13 additions & 17 deletions frontend/src/components/nav/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import TCButton from "components/files/TCButton";
import { useAlertQueue } from "hooks/alerts";
import { api } from "hooks/api";
import { useAuthentication } from "hooks/auth";
import { FormEvent, useEffect, useState } from "react";
import { FormEvent, useState } from "react";
import { Col, Form, Offcanvas, Row } from "react-bootstrap";
import { Link } from "react-router-dom";

Expand All @@ -14,8 +14,6 @@ interface Props {
const Sidebar = ({ show, onHide }: Props) => {
const { addAlert } = useAlertQueue();

const [needToCall, setNeedToCall] = useState<boolean>(true);
const [email, setEmail] = useState<string>("");
const auth = useAuthentication();
const auth_api = new api(auth.api);

Expand Down Expand Up @@ -54,19 +52,6 @@ const Sidebar = ({ show, onHide }: Props) => {
}
};

useEffect(() => {
(async () => {
if (needToCall) {
setNeedToCall(false);
try {
const res = await auth_api.me();
setEmail(res.email);
} catch (error) {
console.error(error);
}
}
})();
}, []);
return (
<Offcanvas show={show} onHide={onHide} placement="end">
<Offcanvas.Header closeButton>
Expand All @@ -84,7 +69,14 @@ const Sidebar = ({ show, onHide }: Props) => {
<p>
<strong>Change Email</strong>
</p>
<p>Current email: {email}</p>
{auth.email == "[email protected]" ? (
<p>
No email address associated with this account. (This is because
you registered via OAuth.)
</p>
) : (
<p>Current email: {auth.email}</p>
)}
{changeEmailSuccess ? (
<p>An email has been sent to your new email address.</p>
) : (
Expand All @@ -109,6 +101,10 @@ const Sidebar = ({ show, onHide }: Props) => {
<p>
<strong>Change Password</strong>
</p>
<p>
You may only change your password if you have a previous password.
If not, log out and reset your password.
</p>
{changePasswordSuccess ? (
<p>Your password has been changed.</p>
) : (
Expand Down
31 changes: 27 additions & 4 deletions frontend/src/components/nav/TopNavbar.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,39 @@
import Boop from "components/nav/Boop";
import Sidebar from "components/nav/Sidebar";
import { useAuthentication } from "hooks/auth";
import { api } from "hooks/api";
import { setLocalStorageAuth, useAuthentication } from "hooks/auth";
import { useTheme } from "hooks/theme";
import { useState } from "react";
import { useEffect, useState } from "react";
import { Container, Nav, Navbar } from "react-bootstrap";
import { GearFill, MoonFill, SunFill } from "react-bootstrap-icons";
import { Link } from "react-router-dom";

const TopNavbar = () => {
const [showSidebar, setShowSidebar] = useState<boolean>(false);
const { theme, setTheme } = useTheme();
const { isAuthenticated } = useAuthentication();
const auth = useAuthentication();
const auth_api = new api(auth.api);

useEffect(() => {
(async () => {
try {
// get code from query string to carry out oauth login
const search = window.location.search;
const params = new URLSearchParams(search);
const code = params.get("code");
if (auth.isAuthenticated) {
const { email } = await auth_api.me();
auth.setEmail(email);
} else if (code) {
const res = await auth_api.login_github(code as string);
setLocalStorageAuth(res.username);
auth.setIsAuthenticated(true);
}
} catch (error) {
console.error(error);
}
})();
}, []);

return (
<>
Expand All @@ -27,7 +50,7 @@ const TopNavbar = () => {
{theme === "dark" ? <MoonFill /> : <SunFill />}
</Nav.Link>
</Boop>
{isAuthenticated ? (
{auth.isAuthenticated ? (
<>
<Boop timing={100}>
<Nav.Link onClick={() => setShowSidebar(true)}>
Expand Down
35 changes: 35 additions & 0 deletions frontend/src/hooks/api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,23 @@ export class api {
}
}

public async send_register_github(): Promise<string> {
try {
const res = await this.api.get("/users/github-login");
return res.data;
} catch (error) {
if (axios.isAxiosError(error)) {
console.error("Error redirecting to github:", error.response?.data);
throw new Error(
error.response?.data?.detail || "Error redirecting to github",
);
} else {
console.error("Unexpected error:", error);
throw new Error("Unexpected error");
}
}
}

public async register(
token: string,
username: string,
Expand Down Expand Up @@ -182,6 +199,24 @@ export class api {
}
}
}

public async login_github(code: string): Promise<MeResponse> {
try {
const res = await this.api.get(`/users/github-code/${code}`);
return res.data;
} catch (error) {
if (axios.isAxiosError(error)) {
console.error("Error logging in:", error.response?.data);
throw new Error(
error.response?.data?.detail || "Error logging in with github",
);
} else {
console.error("Unexpected error:", error);
throw new Error("Unexpected error");
}
}
}

public async logout(): Promise<void> {
try {
await this.api.delete("/users/logout/");
Expand Down
30 changes: 23 additions & 7 deletions frontend/src/hooks/auth.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import axios, { AxiosInstance } from "axios";
import { BACKEND_URL } from "constants/backend";
import { createContext, ReactNode, useCallback, useContext } from "react";
import {
createContext,
ReactNode,
useCallback,
useContext,
useState,
} from "react";
import { useNavigate } from "react-router-dom";

const AUTH_KEY_ID = "AUTH";
Expand All @@ -9,8 +15,9 @@ const getLocalStorageAuth = (): string | null => {
return localStorage.getItem(AUTH_KEY_ID);
};

export const setLocalStorageAuth = (email: string) => {
localStorage.setItem(AUTH_KEY_ID, email);
// changed from email to id to accommodate oauth logins that don't use email
export const setLocalStorageAuth = (id: string) => {
localStorage.setItem(AUTH_KEY_ID, id);
};

export const deleteLocalStorageAuth = () => {
Expand All @@ -20,8 +27,11 @@ export const deleteLocalStorageAuth = () => {
interface AuthenticationContextProps {
logout: () => void;
isAuthenticated: boolean;
email: string | null;
setIsAuthenticated: React.Dispatch<React.SetStateAction<boolean>>;
id: string | null;
api: AxiosInstance;
email: string;
setEmail: React.Dispatch<React.SetStateAction<string>>;
}

const AuthenticationContext = createContext<
Expand All @@ -37,8 +47,11 @@ export const AuthenticationProvider = (props: AuthenticationProviderProps) => {

const navigate = useNavigate();

const isAuthenticated = getLocalStorageAuth() !== null;
const email = getLocalStorageAuth();
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(
getLocalStorageAuth() !== null,
);
const [email, setEmail] = useState<string>("[email protected]");
const id = getLocalStorageAuth();

const api = axios.create({
baseURL: BACKEND_URL,
Expand All @@ -62,8 +75,11 @@ export const AuthenticationProvider = (props: AuthenticationProviderProps) => {
value={{
logout,
isAuthenticated,
email,
setIsAuthenticated,
id,
api,
email,
setEmail,
}}
>
{children}
Expand Down
8 changes: 2 additions & 6 deletions frontend/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { StrictMode } from "react";
// import { StrictMode } from "react";
import ReactDOM from "react-dom/client";

import App from "./App";
Expand All @@ -7,8 +7,4 @@ const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement,
);

root.render(
<StrictMode>
<App />
</StrictMode>,
);
root.render(<App />);
24 changes: 24 additions & 0 deletions frontend/src/pages/Login.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { faGithub } from "@fortawesome/free-brands-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import TCButton from "components/files/TCButton";
import { useAlertQueue } from "hooks/alerts";
import { api } from "hooks/api";
Expand Down Expand Up @@ -32,6 +34,21 @@ const Login = () => {
}
};

const handleGithubSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
try {
const redirectUrl = await auth_api.send_register_github();
window.location.href = redirectUrl;
// setSuccess(true);
} catch (err) {
if (err instanceof Error) {
addAlert(err.message, "error");
} else {
addAlert("Unexpected error.", "error");
}
}
};

return (
<div>
<h1 className="mb-4">Login</h1>
Expand Down Expand Up @@ -65,6 +82,13 @@ const Login = () => {
</div>
<TCButton type="submit">Login</TCButton>
</Form>
<Form onSubmit={handleGithubSubmit}>
<div className="mb-3 mt-3 d-flex justify-content-center">OR</div>
<TCButton type="submit">
<FontAwesomeIcon icon={faGithub} style={{ marginRight: 15 }} />
Login with Github
</TCButton>
</Form>
</div>
);
};
Expand Down
1 change: 1 addition & 0 deletions frontend/src/pages/Logout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const Logout = () => {
deleteLocalStorageAuth();
try {
await auth_api.logout();
auth.setIsAuthenticated(false);
} catch (err) {
console.error(err);
}
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/pages/Register.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import TCButton from "components/files/TCButton";
import { useAlertQueue } from "hooks/alerts";
import { api } from "hooks/api";
import { useAuthentication } from "hooks/auth";
import { setLocalStorageAuth, useAuthentication } from "hooks/auth";
import { FormEvent, useEffect, useState } from "react";
import { Col, Container, Form, Row, Spinner } from "react-bootstrap";
import { Link, useNavigate, useParams } from "react-router-dom";
Expand All @@ -23,6 +23,7 @@ const Register = () => {
event.preventDefault();
try {
await auth_api.register(token || "", username, password);
setLocalStorageAuth(email);
navigate("/");
} catch (err) {
if (err instanceof Error) {
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/pages/RegistrationEmail.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { faGithub } from "@fortawesome/free-brands-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import TCButton from "components/files/TCButton";
import { useAlertQueue } from "hooks/alerts";
import { api } from "hooks/api";
Expand Down Expand Up @@ -28,6 +30,24 @@ const RegistrationEmail = () => {
}
}
};

const handleGithubSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
try {
setSubmitted(true);
const redirectUrl = await auth_api.send_register_github();
window.location.href = redirectUrl;
// setSuccess(true);
} catch (err) {
setSubmitted(false);
if (err instanceof Error) {
addAlert(err.message, "error");
} else {
addAlert("Unexpected error.", "error");
}
}
};

if (success) {
return (
<div>
Expand Down Expand Up @@ -72,6 +92,13 @@ const RegistrationEmail = () => {
/>
<TCButton type="submit">Send Code</TCButton>
</Form>
<Form onSubmit={handleGithubSubmit}>
<div className="mb-3 mt-3 d-flex justify-content-center">OR</div>
<TCButton type="submit">
<FontAwesomeIcon icon={faGithub} style={{ marginRight: 15 }} />
Register with Github
</TCButton>
</Form>
</div>
);
};
Expand Down
Loading

0 comments on commit a7f467c

Please sign in to comment.