diff --git a/client/src/App.tsx b/client/src/App.tsx index 075a107..08a6107 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -4,11 +4,14 @@ import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import MainWindow from './components/MainWindow'; +import { ProvideAuth } from './lib/useAuth'; function App() { return ( - + + + ); } diff --git a/client/src/api.ts b/client/src/api.ts index aec6cb8..df4093c 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -354,4 +354,6 @@ export function checkRegisterUserRequest(req: any): boolean { checkPassword(req.body.password); } -// Note: Login is handled by Passport.js, so it is not explicitly written here. \ No newline at end of file +// Note: Login is handled by Passport.js, so it is not explicitly written here. +export const LoginEndpoint = "/login"; +export const LogoutEndpoint = "/logout"; \ No newline at end of file diff --git a/client/src/components/MainWindow.tsx b/client/src/components/MainWindow.tsx index 2839106..dee2afe 100644 --- a/client/src/components/MainWindow.tsx +++ b/client/src/components/MainWindow.tsx @@ -10,6 +10,8 @@ import TagWindow from './windows/tag/TagWindow'; import SongWindow from './windows/song/SongWindow'; import ManageTagsWindow from './windows/manage_tags/ManageTagsWindow'; import { BrowserRouter, Switch, Route, useParams, Redirect } from 'react-router-dom'; +import LoginWindow from './windows/login/LoginWindow'; +import { useAuth } from '../lib/useAuth'; var _ = require('lodash'); const darkTheme = createMuiTheme({ @@ -21,6 +23,25 @@ const darkTheme = createMuiTheme({ }, }); +function PrivateRoute(props: any) { + const { children, ...rest } = props; + let auth = useAuth(); + return + auth.user ? ( + children + ) : ( + + ) + } + /> +} + export default function MainWindow(props: any) { return @@ -29,30 +50,34 @@ export default function MainWindow(props: any) { - - - - - + - + - + + + + + - - - + + + - - - + + + - - - + + + + + + + - - + + diff --git a/client/src/components/windows/Windows.tsx b/client/src/components/windows/Windows.tsx index edd6768..a75ceac 100644 --- a/client/src/components/windows/Windows.tsx +++ b/client/src/components/windows/Windows.tsx @@ -20,6 +20,7 @@ export enum WindowType { Tag = "Tag", Song = "Song", ManageTags = "ManageTags", + Login = "Login", } export interface WindowState { } diff --git a/client/src/components/windows/login/LoginWindow.tsx b/client/src/components/windows/login/LoginWindow.tsx new file mode 100644 index 0000000..5631852 --- /dev/null +++ b/client/src/components/windows/login/LoginWindow.tsx @@ -0,0 +1,70 @@ +import React, { useState } from 'react'; +import { WindowState } from "../Windows"; +import { Box, Paper, Typography, TextField, Button } from "@material-ui/core"; +import { useHistory, useLocation } from 'react-router'; +import { useAuth, Auth } from '../../../lib/useAuth'; + +export interface LoginWindowState extends WindowState { } +export enum LoginWindowStateActions { } +export function LoginWindowReducer(state: LoginWindowState, action: any) { } + +export default function LoginWindow(props: {}) { + let history: any = useHistory(); + let location: any = useLocation(); + let auth: Auth = useAuth(); + let { from } = location.state || { from: { pathname: "/" } }; + + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + + const onSubmit = (event: any) => { + event.preventDefault(); + auth.signin(email, password) + .then(() => { + history.replace(from); + }) + } + + return + + + + Sign in +
+ setEmail(e.target.value)} + /> + setPassword(e.target.value)} + /> + + +
+
+
+
+} \ No newline at end of file diff --git a/client/src/lib/useAuth.tsx b/client/src/lib/useAuth.tsx new file mode 100644 index 0000000..6373901 --- /dev/null +++ b/client/src/lib/useAuth.tsx @@ -0,0 +1,140 @@ +// Note: Based on https://usehooks.com/useAuth/ + +// import React from "react"; +// import { ProvideAuth } from "./use-auth.js"; +// function App(props) { +// return ( +// +// {/* +// Route components here, depending on how your app is structured. +// If using Next.js this would be /pages/_app.js +// */} +// +// ); +// } + +// // Any component that wants auth state +// import React from "react"; +// import { useAuth } from "./use-auth.js"; + +// function Navbar(props) { +// // Get auth state and re-render anytime it changes +// const auth = useAuth(); + +// return ( +// +// +// +// About +// Contact +// {auth.user ? ( +// +// Account ({auth.user.email}) +// +// +// ) : ( +// Signin +// )} +// +// +// ); +// } + +// Hook (use-auth.js) + +import React, { useState, useEffect, useContext, createContext } from "react"; +import * as serverApi from '../api'; + +export interface AuthUser { + id: number, +} + +export interface Auth { + user: AuthUser | null, + signout: () => void, + signin: (email: string, password: string) => Promise, + signup: (email: string, password: string) => void, +}; + +const authContext = createContext({ + user: null, + signout: () => { }, + signin: (email: string, password: string) => { + throw new Error("Auth object not initialized."); + }, + signup: (email: string, password: string) => { + throw new Error("Auth object not initialized."); + }, +}); + +export function ProvideAuth(props: { children: any }) { + const auth = useProvideAuth(); + return {props.children}; +} + +export const useAuth = () => { + return useContext(authContext); +}; + +function useProvideAuth() { + const [user, setUser] = useState(null); + + // FIXME: password shouldn't be encoded into the URL. + const signin = (email: string, password: string) => { + return (async () => { + const urlBase = (process.env.REACT_APP_BACKEND || "") + serverApi.LoginEndpoint; + const url = `${urlBase}?username=${encodeURIComponent(email)}&password=${encodeURIComponent(password)}`; + + const response = await fetch(url, { method: "POST" }); + const json = await response.json(); + if(!("userId" in json)) { + throw new Error("No UserID received from login."); + } + + const user = { + id: json.userId + } + setUser(user); + return user; + })(); + }; + + const signup = (email: string, password: string) => { + return (async () => { + const requestOpts = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: email, + password: password, + }) + }; + + return (async () => { + const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.RegisterUserEndpoint, requestOpts) + if(!response.ok) { + throw new Error("Failed to register user.") + } + })(); + })(); + }; + + const signout = () => { + return (async () => { + const url = (process.env.REACT_APP_BACKEND || "") + serverApi.LogoutEndpoint; + const response = await fetch(url, { method: "POST" }); + if(!response.ok) { + throw new Error("Failed to log out."); + } + setUser(null); + })(); + }; + + // Return the user object and auth methods + return { + user, + signin, + signup, + signout, + }; +} \ No newline at end of file diff --git a/server/app.ts b/server/app.ts index a4302bb..5a34d00 100644 --- a/server/app.ts +++ b/server/app.ts @@ -87,7 +87,10 @@ const SetupApp = (app: any, knex: Knex, apiBaseUrl: string) => { const checkLogin = () => { return function (req: any, res: any, next: any) { if (!req.isAuthenticated || !req.isAuthenticated()) { - return res.status(401).send(); + return res + .status(401) + .json({ reason: "NotLoggedIn" }) + .send(); } next(); } @@ -111,7 +114,9 @@ const SetupApp = (app: any, knex: Knex, apiBaseUrl: string) => { app.post(apiBaseUrl + api.MergeTagEndpoint, checkLogin(), _invoke(MergeTagEndpointHandler)); app.post(apiBaseUrl + api.RegisterUserEndpoint, _invoke(RegisterUserEndpointHandler)); - app.post('/login', passport.authenticate('local'), (req: any, res: any) => { res.status(200).send(); }); + app.post('/login', passport.authenticate('local'), (req: any, res: any) => { + res.status(200).send({ userId: req.user.id }); + }); app.post('/logout', function (req: any, res: any) { req.logout(); res.status(200).send();