Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented Github OAuth feature #158

Merged
merged 9 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading