Merge pull request #1 from innocuous-symmetry/api-with-jwt
Api with jwt
This commit is contained in:
@@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tinymce/tinymce-react": "^4.2.0",
|
"@tinymce/tinymce-react": "^4.2.0",
|
||||||
"axios": "^1.2.0",
|
"axios": "^1.2.0",
|
||||||
|
"jwt-decode": "^3.1.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.4.3",
|
"react-router-dom": "^6.4.3",
|
||||||
@@ -18,6 +19,7 @@
|
|||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/jwt-decode": "^3.1.0",
|
||||||
"@types/react": "^18.0.24",
|
"@types/react": "^18.0.24",
|
||||||
"@types/react-dom": "^18.0.8",
|
"@types/react-dom": "^18.0.8",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
// framework tools and custom utils
|
// framework tools and custom utils
|
||||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||||
import { AuthContext, IAuthContext, useAuthContext } from './context/AuthContext';
|
import { useAuthContext } from './context/AuthContext';
|
||||||
import { attemptLogout, checkCredientials } from './util/apiUtils';
|
import jwtDecode from 'jwt-decode';
|
||||||
import { IUser } from './schemas';
|
import API from './util/API';
|
||||||
|
|
||||||
// pages, ui, styles
|
// pages, ui, components, styles
|
||||||
import Subscriptions from './components/pages/Subscriptions/Subscriptions';
|
import Subscriptions from './components/pages/Subscriptions/Subscriptions';
|
||||||
import Browser from './components/ui/Browser';
|
import Browser from './components/ui/Browser';
|
||||||
import Collection from './components/pages/Collection';
|
import Collection from './components/pages/Collection';
|
||||||
@@ -19,55 +19,51 @@ import CollectionBrowser from './components/pages/CollectionBrowser';
|
|||||||
import { Navbar } from './components/ui';
|
import { Navbar } from './components/ui';
|
||||||
import GroceryList from './components/pages/GroceryList';
|
import GroceryList from './components/pages/GroceryList';
|
||||||
import GroceryListCollection from './components/pages/GroceryListCollection';
|
import GroceryListCollection from './components/pages/GroceryListCollection';
|
||||||
|
import { TokenType } from './util/types';
|
||||||
import './sass/App.scss';
|
import './sass/App.scss';
|
||||||
|
import handleToken from './util/handleToken';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [user, setUser] = useState<any>();
|
const { setUser, token, setToken } = useAuthContext();
|
||||||
const parentState = { user, setUser };
|
|
||||||
|
|
||||||
const receiveChange = (() => {});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const wrapper = async () => {
|
if (document.cookie) {
|
||||||
try {
|
const response = handleToken();
|
||||||
const result: IAuthContext | undefined = await checkCredientials();
|
if (response) {
|
||||||
|
setToken(response.token);
|
||||||
if (result == undefined) {
|
setUser(response.user);
|
||||||
setUser({ user: undefined });
|
|
||||||
} else {
|
|
||||||
setUser(result);
|
|
||||||
}
|
|
||||||
} catch(e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}, [document.cookie]);
|
||||||
|
|
||||||
wrapper();
|
useEffect(() => {
|
||||||
}, [])
|
if (token) {
|
||||||
|
const response = handleToken();
|
||||||
|
response && setUser(response.user);
|
||||||
|
}
|
||||||
|
}, [setToken])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<AuthContext.Provider value={ parentState }>
|
<div className="App">
|
||||||
<div className="App">
|
<Navbar />
|
||||||
<Navbar receiveChange={receiveChange} />
|
<Routes>
|
||||||
<Routes>
|
<Route path="/" element={<Welcome />} />
|
||||||
<Route path="/" element={<Welcome />} />
|
<Route path="/register" element={<Register />} />
|
||||||
<Route path="/register" element={<Register receiveChange={receiveChange} />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/profile" element={<Profile />} />
|
||||||
<Route path="/profile" element={<Profile />} />
|
<Route path="/collections" element={<CollectionBrowser />} />
|
||||||
<Route path="/collections" element={<CollectionBrowser />} />
|
<Route path="/collections/:id" element={<Collection />} />
|
||||||
<Route path="/collections/:id" element={<Collection />} />
|
<Route path="/explore" element={<Browser header="" searchFunction={() => {}} />} />
|
||||||
<Route path="/explore" element={<Browser header="" searchFunction={() => {}} />} />
|
<Route path="/recipe/:id" element={<Recipe />} />
|
||||||
<Route path="/recipe/:id" element={<Recipe />} />
|
<Route path="/subscriptions" element={<Subscriptions />} />
|
||||||
<Route path="/subscriptions" element={<Subscriptions />} />
|
<Route path="/subscriptions/:id" element={<Collection />} />
|
||||||
<Route path="/subscriptions/:id" element={<Collection />} />
|
|
||||||
|
|
||||||
<Route path="/add-recipe" element={<AddRecipe />} />
|
<Route path="/add-recipe" element={<AddRecipe />} />
|
||||||
<Route path="/grocery-list" element={<GroceryListCollection />} />
|
<Route path="/grocery-list" element={<GroceryListCollection />} />
|
||||||
<Route path="/grocery-list/:id" element={<GroceryList />} />
|
<Route path="/grocery-list/:id" element={<GroceryList />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</AuthContext.Provider>
|
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const AddRecipe = () => {
|
|||||||
|
|
||||||
const getFormState = useCallback((data: IRecipe) => {
|
const getFormState = useCallback((data: IRecipe) => {
|
||||||
setInput(data);
|
setInput(data);
|
||||||
}, [])
|
}, [input])
|
||||||
|
|
||||||
const handleCreate = () => {
|
const handleCreate = () => {
|
||||||
for (let field of Object.keys(input)) {
|
for (let field of Object.keys(input)) {
|
||||||
@@ -29,20 +29,6 @@ const AddRecipe = () => {
|
|||||||
})
|
})
|
||||||
}, [authContext])
|
}, [authContext])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
input.authoruserid && setForm(
|
|
||||||
new Form<IRecipe>({
|
|
||||||
parent: "AddRecipe",
|
|
||||||
keys: ["name", "preptime", "course", "cuisine", "ingredients", "description"],
|
|
||||||
labels: ["Recipe Name:", "Prep Time:", "Course:", "Cuisine:", "Ingredients:", "Description:"],
|
|
||||||
dataTypes: ['text', 'text', 'custom picker', 'custom picker', 'custom picker', 'TINYMCE'],
|
|
||||||
initialState: input,
|
|
||||||
getState: getFormState,
|
|
||||||
richTextInitialValue: "<p>Enter recipe details here!</p>"
|
|
||||||
}).mount()
|
|
||||||
)
|
|
||||||
}, [input.authoruserid])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(input);
|
console.log(input);
|
||||||
}, [input])
|
}, [input])
|
||||||
@@ -53,7 +39,18 @@ const AddRecipe = () => {
|
|||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Panel>
|
<Panel>
|
||||||
|
<Form parent={input} _config={{
|
||||||
|
parent: "AddRecipe",
|
||||||
|
keys: ["name", "preptime", "course", "cuisine", "ingredients", "description"],
|
||||||
|
labels: ["Recipe Name:", "Prep Time:", "Course:", "Cuisine:", "Ingredients:", "Description:"],
|
||||||
|
dataTypes: ['text', 'text', 'custom picker', 'custom picker', 'custom picker', 'TINYMCE'],
|
||||||
|
initialState: input,
|
||||||
|
getState: getFormState,
|
||||||
|
richTextInitialValue: "<p>Enter recipe details here!</p>"
|
||||||
|
}} />
|
||||||
|
|
||||||
{ form || <h2>Loading...</h2> }
|
{ form || <h2>Loading...</h2> }
|
||||||
|
|
||||||
<Button onClick={handleCreate}>Create Recipe!</Button>
|
<Button onClick={handleCreate}>Create Recipe!</Button>
|
||||||
</Panel>
|
</Panel>
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
@@ -1,46 +1,40 @@
|
|||||||
import { useCallback, useContext, useEffect, useState } from "react";
|
import { useCallback, useContext, useEffect, useState } from "react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
|
||||||
import { AuthContext, useAuthContext } from "../../context/AuthContext";
|
import { AuthContext, useAuthContext } from "../../context/AuthContext";
|
||||||
import { attemptLogin } from "../../util/apiUtils";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { IUserAuth } from "../../schemas";
|
import { IUser, IUserAuth } from "../../schemas";
|
||||||
import { Button, Form, Page, Panel } from "../ui";
|
import { Button, Form, Page, Panel } from "../ui";
|
||||||
|
import { FormConfig } from "../ui/Form";
|
||||||
|
import API from "../../util/API";
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const redirect = params.get("redirect");
|
const redirect = params.get("redirect");
|
||||||
const { user, setUser } = useContext(AuthContext);
|
const { user, setToken } = useContext(AuthContext);
|
||||||
|
|
||||||
// setup and local state
|
// setup and local state
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [form, setForm] = useState<JSX.Element>();
|
|
||||||
const [input, setInput] = useState<IUserAuth>({ email: '', password: '' });
|
const [input, setInput] = useState<IUserAuth>({ email: '', password: '' });
|
||||||
|
|
||||||
// retrieve and store state from form
|
// retrieve and store state from form
|
||||||
const getFormState = useCallback((received: IUserAuth) => {
|
const getFormState = useCallback((received: IUserAuth) => {
|
||||||
setInput(received);
|
setInput(received);
|
||||||
}, [])
|
}, [input])
|
||||||
|
|
||||||
|
|
||||||
const handleLogin = async () => {
|
const handleLogin = async () => {
|
||||||
if (!input.email || !input.password) return;
|
if (!input.email || !input.password) return;
|
||||||
const { data, ok } = await attemptLogin(input);
|
const result = await new API.Auth().login(input);
|
||||||
if (ok) setUser(data);
|
|
||||||
|
// setting token will trigger ui update
|
||||||
|
setToken(result.token);
|
||||||
|
|
||||||
|
// if there is a redirect, go there, else go home
|
||||||
navigate(`/${redirect ?? ''}`);
|
navigate(`/${redirect ?? ''}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for logged in user and mount form
|
// check for logged in user and mount form
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) navigate('/');
|
if (user) navigate('/');
|
||||||
setForm(
|
|
||||||
new Form<IUserAuth>({
|
|
||||||
parent: 'login',
|
|
||||||
keys: Object.keys(input),
|
|
||||||
labels: ["Email", "Password"],
|
|
||||||
dataTypes: Object.keys(input),
|
|
||||||
initialState: input,
|
|
||||||
getState: getFormState
|
|
||||||
}).mount()
|
|
||||||
);
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -48,8 +42,18 @@ export default function Login() {
|
|||||||
<h1>Hello! Nice to see you again.</h1>
|
<h1>Hello! Nice to see you again.</h1>
|
||||||
|
|
||||||
<Panel extraStyles="form-panel">
|
<Panel extraStyles="form-panel">
|
||||||
{ form || <h2>Loading...</h2> }
|
|
||||||
|
<Form parent={input} _config={{
|
||||||
|
parent: 'login',
|
||||||
|
keys: Object.keys(input),
|
||||||
|
labels: ["Email", "Password"],
|
||||||
|
dataTypes: Object.keys(input),
|
||||||
|
initialState: input,
|
||||||
|
getState: getFormState
|
||||||
|
}} />
|
||||||
|
|
||||||
<Button onClick={handleLogin}>Log In</Button>
|
<Button onClick={handleLogin}>Log In</Button>
|
||||||
|
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<aside>Not registered yet? You can do that <a href="/register">here.</a></aside>
|
<aside>Not registered yet? You can do that <a href="/register">here.</a></aside>
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ export default function Profile() {
|
|||||||
return (
|
return (
|
||||||
<Protect redirect="profile">
|
<Protect redirect="profile">
|
||||||
<div className="profile-authenticated">
|
<div className="profile-authenticated">
|
||||||
<h1>{user?.firstname}'s Profile</h1>
|
<h1>{user && user.firstname}'s Profile</h1>
|
||||||
<p>Things and stuff!</p>
|
|
||||||
<Friends />
|
<Friends />
|
||||||
</div>
|
</div>
|
||||||
</Protect>
|
</Protect>
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import { RegisterVariantType, VariantLabel } from ".";
|
|||||||
import { useAuthContext } from "../../../context/AuthContext";
|
import { useAuthContext } from "../../../context/AuthContext";
|
||||||
import { IUser, IUserAuth } from "../../../schemas";
|
import { IUser, IUserAuth } from "../../../schemas";
|
||||||
import { attemptLogin, attemptRegister } from "../../../util/apiUtils";
|
import { attemptLogin, attemptRegister } from "../../../util/apiUtils";
|
||||||
|
import API from "../../../util/API";
|
||||||
import { Button, Page, Panel } from "../../ui";
|
import { Button, Page, Panel } from "../../ui";
|
||||||
import Divider from "../../ui/Divider";
|
import Divider from "../../ui/Divider";
|
||||||
import Form, { FormConfig } from "../../ui/Form";
|
import Form from "../../ui/Form";
|
||||||
|
|
||||||
const blankUser: IUser = {
|
const blankUser: IUser = {
|
||||||
firstname: '',
|
firstname: '',
|
||||||
@@ -20,51 +21,31 @@ const blankUser: IUser = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const AboutYou: RegisterVariantType = ({ transitionDisplay }) => {
|
const AboutYou: RegisterVariantType = ({ transitionDisplay }) => {
|
||||||
|
const auth = new API.Auth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const authContext = useAuthContext();
|
const { user, setToken } = useAuthContext();
|
||||||
const [form, setForm] = useState<JSX.Element>(<p key={v4()}>Loading content...</p>);
|
|
||||||
const [input, setInput] = useState<IUser>(blankUser);
|
const [input, setInput] = useState<IUser>(blankUser);
|
||||||
const [regSuccess, setRegSuccess] = useState<any>();
|
|
||||||
|
|
||||||
const getFormState = useCallback((received: IUser) => {
|
const getFormState = useCallback((received: IUser) => {
|
||||||
setInput(received);
|
setInput(received);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (authContext.user) navigate('/');
|
if (user) navigate('/');
|
||||||
}, [authContext]);
|
}, [user]);
|
||||||
|
|
||||||
async function handleRegister() {
|
async function handleRegister() {
|
||||||
const res = await attemptRegister(input);
|
const res = await auth.register(input);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
setTimeout(async () => {
|
||||||
|
const result = await auth.login(input);
|
||||||
|
setToken(result.token);
|
||||||
|
}, 750);
|
||||||
|
|
||||||
transitionDisplay(VariantLabel.InitialCollection, input);
|
transitionDisplay(VariantLabel.InitialCollection, input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function unwrapLogin() {
|
|
||||||
const data: IUserAuth = { email: input.email, password: input.password || "" }
|
|
||||||
const login = await attemptLogin(data);
|
|
||||||
if (login) {
|
|
||||||
authContext.user = login.user;
|
|
||||||
}
|
|
||||||
navigate('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setForm(new Form<IUser>({
|
|
||||||
parent: "register",
|
|
||||||
keys: ['firstname', 'lastname', 'handle', 'email', 'password'],
|
|
||||||
initialState: input,
|
|
||||||
labels: ['First Name', 'Last Name', 'Handle', 'Email', "Password"],
|
|
||||||
dataTypes: ['text', 'text', 'text', 'email', 'password'],
|
|
||||||
getState: getFormState
|
|
||||||
}).mount());
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (regSuccess) unwrapLogin();
|
|
||||||
}, [regSuccess])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<h1>Hi! Thanks for being here.</h1>
|
<h1>Hi! Thanks for being here.</h1>
|
||||||
@@ -74,7 +55,16 @@ const AboutYou: RegisterVariantType = ({ transitionDisplay }) => {
|
|||||||
<h2>Tell us a bit about yourself:</h2>
|
<h2>Tell us a bit about yourself:</h2>
|
||||||
|
|
||||||
<Panel extraStyles="form-panel two-columns">
|
<Panel extraStyles="form-panel two-columns">
|
||||||
{ form || <h2>Loading...</h2> }
|
|
||||||
|
<Form parent={input} _config={{
|
||||||
|
parent: "register",
|
||||||
|
keys: ['firstname', 'lastname', 'handle', 'email', 'password'],
|
||||||
|
initialState: input,
|
||||||
|
labels: ['First Name', 'Last Name', 'Handle', 'Email', "Password"],
|
||||||
|
dataTypes: ['text', 'text', 'text', 'email', 'password'],
|
||||||
|
getState: getFormState
|
||||||
|
}} />
|
||||||
|
|
||||||
<Button onClick={handleRegister}>Register</Button>
|
<Button onClick={handleRegister}>Register</Button>
|
||||||
</Panel>
|
</Panel>
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
@@ -3,33 +3,24 @@ import { RegisterVariantType, VariantLabel } from ".";
|
|||||||
import { useNow } from "../../../hooks/useNow";
|
import { useNow } from "../../../hooks/useNow";
|
||||||
import { ICollection, IUser, IUserAuth } from "../../../schemas";
|
import { ICollection, IUser, IUserAuth } from "../../../schemas";
|
||||||
import { attemptLogin, createNewCollection } from "../../../util/apiUtils";
|
import { attemptLogin, createNewCollection } from "../../../util/apiUtils";
|
||||||
|
import API from "../../../util/API";
|
||||||
import { Button, Divider, Page, Panel } from "../../ui";
|
import { Button, Divider, Page, Panel } from "../../ui";
|
||||||
import TextField from "../../ui/TextField";
|
import TextField from "../../ui/TextField";
|
||||||
|
import { useAuthContext } from "../../../context/AuthContext";
|
||||||
|
|
||||||
const InitialCollection: RegisterVariantType = ({ transitionDisplay, receiveChange, input }) => {
|
const InitialCollection: RegisterVariantType = ({ transitionDisplay, input }) => {
|
||||||
|
const { user, token } = useAuthContext();
|
||||||
const [collectionName, setCollectionName] = useState<string>();
|
const [collectionName, setCollectionName] = useState<string>();
|
||||||
const [view, setView] = useState<JSX.Element>(<Page><h1>Loading...</h1></Page>);
|
const [view, setView] = useState<JSX.Element>(<Page><h1>Loading...</h1></Page>);
|
||||||
const [user, setUser] = useState<IUser>();
|
|
||||||
const now = useNow();
|
const now = useNow();
|
||||||
|
|
||||||
async function unwrapLogin(data: IUser) {
|
|
||||||
const userInfo: IUserAuth = { email: data.email, password: data.password! }
|
|
||||||
const login = await attemptLogin(userInfo);
|
|
||||||
setUser(login.user);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (input) {
|
|
||||||
setTimeout(() => {
|
|
||||||
unwrapLogin(input);
|
|
||||||
}, 750);
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleClick = async () => {
|
const handleClick = async () => {
|
||||||
if (!user) return;
|
if (!user || !token) return;
|
||||||
|
|
||||||
|
const collectionAPI = new API.Collection(token);
|
||||||
|
|
||||||
const collection: ICollection = {
|
const collection: ICollection = {
|
||||||
name: collectionName || (user.firstname + "'s Collection"),
|
name: collectionName ?? (user.firstname + "'s Collection"),
|
||||||
active: true,
|
active: true,
|
||||||
ismaincollection: true,
|
ismaincollection: true,
|
||||||
ownerid: user.id!.toString(),
|
ownerid: user.id!.toString(),
|
||||||
@@ -39,14 +30,13 @@ const InitialCollection: RegisterVariantType = ({ transitionDisplay, receiveChan
|
|||||||
|
|
||||||
console.log(collection);
|
console.log(collection);
|
||||||
|
|
||||||
const result = await createNewCollection(collection);
|
const result = await collectionAPI.post(collection);
|
||||||
console.log(result);
|
console.log(result);
|
||||||
if (result) transitionDisplay(VariantLabel.AddFriends);
|
if (result) transitionDisplay(VariantLabel.AddFriends);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user && receiveChange) {
|
if (user && token) {
|
||||||
receiveChange(user);
|
|
||||||
setView(
|
setView(
|
||||||
<Page>
|
<Page>
|
||||||
<h1>Hi, {user.firstname}! Great to meet you.</h1>
|
<h1>Hi, {user.firstname}! Great to meet you.</h1>
|
||||||
@@ -59,7 +49,8 @@ const InitialCollection: RegisterVariantType = ({ transitionDisplay, receiveChan
|
|||||||
<Divider />
|
<Divider />
|
||||||
<h3>What would you like to call your main collection?</h3>
|
<h3>What would you like to call your main collection?</h3>
|
||||||
|
|
||||||
<TextField onChange={(e: ChangeEvent<HTMLInputElement>) => setCollectionName(e.target.value)} placeholder={user.firstname + 's Collection'} />
|
{/* <TextField onChange={(e: ChangeEvent<HTMLInputElement>) => setCollectionName(e.target.value)} placeholder={user.firstname + 's Collection'} /> */}
|
||||||
|
<input type="text" onChange={(e) => setCollectionName(e.target.value)} placeholder={user.firstname + 's Collection'}></input>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<Button onClick={handleClick}>Next</Button>
|
<Button onClick={handleClick}>Next</Button>
|
||||||
|
|||||||
@@ -19,17 +19,17 @@ export enum VariantLabel {
|
|||||||
FinishUp
|
FinishUp
|
||||||
}
|
}
|
||||||
|
|
||||||
const Register: FC<{receiveChange: (change: IUser) => void}> = ({ receiveChange }) => {
|
const Register = () => {
|
||||||
const [displayed, setDisplayed] = useState<JSX.Element>();
|
const [displayed, setDisplayed] = useState<JSX.Element>();
|
||||||
const authContext = useAuthContext();
|
const { user } = useAuthContext();
|
||||||
|
|
||||||
const transitionDisplay = (variant: number | VariantLabel, user?: IUser) => {
|
const transitionDisplay = (variant: number | VariantLabel) => {
|
||||||
switch (variant) {
|
switch (variant) {
|
||||||
case 0:
|
case 0:
|
||||||
setDisplayed(<AboutYou transitionDisplay={transitionDisplay} />);
|
setDisplayed(<AboutYou transitionDisplay={transitionDisplay} />);
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
setDisplayed(<InitialCollection transitionDisplay={transitionDisplay} input={user} receiveChange={receiveChange} />);
|
setDisplayed(<InitialCollection transitionDisplay={transitionDisplay} input={user} />);
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
setDisplayed(<AddFriends transitionDisplay={transitionDisplay} input={user} />);
|
setDisplayed(<AddFriends transitionDisplay={transitionDisplay} input={user} />);
|
||||||
@@ -38,7 +38,7 @@ const Register: FC<{receiveChange: (change: IUser) => void}> = ({ receiveChange
|
|||||||
setDisplayed(<FinishUp transitionDisplay={transitionDisplay} />);
|
setDisplayed(<FinishUp transitionDisplay={transitionDisplay} />);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
setDisplayed(<AboutYou transitionDisplay={transitionDisplay} input={authContext.user || user} />);
|
setDisplayed(<AboutYou transitionDisplay={transitionDisplay} input={user} />);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
import { ChangeEvent, FC } from "react";
|
import { ChangeEvent, FC, useEffect, useState } from "react"
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from "uuid"
|
||||||
import RichText from "./RichText";
|
import RichText from "./RichText"
|
||||||
|
|
||||||
/**
|
|
||||||
* For the generation of more complex form objects with
|
|
||||||
* larger stateful values; expects to receive an object of
|
|
||||||
* type T to a form which can mutate T with a state setter
|
|
||||||
* of type Dispatch<SetStateAction<T>>
|
|
||||||
**/
|
|
||||||
|
|
||||||
export interface FormConfig<T> {
|
export interface FormConfig<T> {
|
||||||
parent: string
|
parent: string
|
||||||
@@ -20,83 +13,95 @@ export interface FormConfig<T> {
|
|||||||
extraStyles?: string
|
extraStyles?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Form<T> {
|
interface FormProps {
|
||||||
private parent: string;
|
parent: any
|
||||||
private labels: string[];
|
_config: FormConfig<any>
|
||||||
private keys: string[];
|
|
||||||
private dataTypes: any[]
|
|
||||||
private state: T;
|
|
||||||
private getState: (received: T) => void
|
|
||||||
private richTextInitialValue?: string;
|
|
||||||
private extraStyles?: string
|
|
||||||
|
|
||||||
constructor(config: FormConfig<T>){
|
|
||||||
this.parent = config.parent;
|
|
||||||
this.keys = config.keys;
|
|
||||||
this.labels = config.labels || this.keys;
|
|
||||||
this.dataTypes = config.dataTypes || new Array(this.keys.length).fill('text');
|
|
||||||
this.state = config.initialState;
|
|
||||||
this.getState = config.getState;
|
|
||||||
this.richTextInitialValue = config.richTextInitialValue;
|
|
||||||
this.extraStyles = config.extraStyles;
|
|
||||||
|
|
||||||
this.mount();
|
|
||||||
}
|
|
||||||
|
|
||||||
update(e: ChangeEvent<HTMLElement>, idx: number) {
|
|
||||||
let newState = {
|
|
||||||
...this.state,
|
|
||||||
[this.keys[idx]]: e.target['value' as keyof EventTarget]
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state = newState;
|
|
||||||
this.getState(newState);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateRichText(txt: string, idx: number) {
|
|
||||||
this.state = {
|
|
||||||
...this.state,
|
|
||||||
[this.keys[idx]]: txt
|
|
||||||
}
|
|
||||||
|
|
||||||
this.getState(this.state);
|
|
||||||
}
|
|
||||||
|
|
||||||
mount() {
|
|
||||||
let output = new Array<JSX.Element>();
|
|
||||||
|
|
||||||
for (let i = 0; i < this.keys.length; i++) {
|
|
||||||
let input: JSX.Element | null;
|
|
||||||
|
|
||||||
if (this.dataTypes[i] == 'custom picker') {
|
|
||||||
console.log('noted!');
|
|
||||||
this.dataTypes[i] = 'text';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.dataTypes[i] == 'TINYMCE') {
|
|
||||||
input = (
|
|
||||||
<div id={`${this.parent}-row-${i}`} key={v4()}>
|
|
||||||
<label htmlFor={`${this.parent}-${this.keys[i]}`}>{this.labels[i]}</label>
|
|
||||||
<RichText id={`${this.parent}-${this.keys[i]}`} initialValue={this.richTextInitialValue} getState={(txt) => this.updateRichText(txt, i)} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
input = (
|
|
||||||
<div id={`${this.parent}-row-${i}`} key={v4()}>
|
|
||||||
<label htmlFor={`${this.parent}-${this.keys[i]}`}>{this.labels[i]}</label>
|
|
||||||
<input
|
|
||||||
type={this.dataTypes[i]}
|
|
||||||
id={`${this.parent}-${this.keys[i]}`}
|
|
||||||
onChange={(e) => this.update(e, i)}
|
|
||||||
value={this.state[i as keyof T] as string}>
|
|
||||||
</input>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
output.push(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className={`ui-form-component ${this.extraStyles}`}>{output}</div>;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Form: FC<FormProps> = ({ parent, _config }) => {
|
||||||
|
type T = typeof parent;
|
||||||
|
const { getState } = _config;
|
||||||
|
|
||||||
|
const [config, setConfig] = useState<FormConfig<T>>();
|
||||||
|
const [state, setState] = useState<T>();
|
||||||
|
const [contents, setContents] = useState<JSX.Element[]>();
|
||||||
|
|
||||||
|
// initial setup
|
||||||
|
useEffect(() => {
|
||||||
|
if (!config) setConfig({
|
||||||
|
..._config,
|
||||||
|
labels: _config.labels ?? _config.keys,
|
||||||
|
dataTypes: _config.dataTypes ?? new Array(_config.keys?.length).fill("text"),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!state) setState(_config.initialState);
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// usecallback handling
|
||||||
|
useEffect(() => {
|
||||||
|
state && getState(state);
|
||||||
|
}, [state]);
|
||||||
|
|
||||||
|
// update methods
|
||||||
|
function updateRichText(txt: string, idx: number) {
|
||||||
|
if (!config) return;
|
||||||
|
|
||||||
|
setState((prev: T) => {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[config.keys[idx]]: txt
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(e: ChangeEvent<HTMLElement>, idx: number) {
|
||||||
|
if (!config) return;
|
||||||
|
|
||||||
|
setState((prev: T) => {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[config.keys[idx]]: e.target['value' as keyof EventTarget]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// mount the form once config has been loaded
|
||||||
|
useEffect(() => {
|
||||||
|
if (state && config) {
|
||||||
|
const result = config.keys.map((each: string, i: number) => {
|
||||||
|
|
||||||
|
if (config.dataTypes![i] == 'TINYMCE') {
|
||||||
|
return (
|
||||||
|
<div id={`${config.parent}-row-${i}`} key={v4()}>
|
||||||
|
<label htmlFor={`${config.parent}-${each}`}>{config.labels![i]}</label>
|
||||||
|
<RichText id={`${config.parent}-${each}`} initialValue={config.richTextInitialValue} getState={(txt) => updateRichText(txt, i)} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div id={`${config.parent}-row-${i}`} key={v4()}>
|
||||||
|
<label htmlFor={`${config.parent}-${each}`}>{config.labels![i]}</label>
|
||||||
|
<input
|
||||||
|
type={config.dataTypes![i]}
|
||||||
|
id={`${config.parent}-${each}`}
|
||||||
|
onChange={(e) => update(e, i)}
|
||||||
|
value={state[i as keyof T] as string}>
|
||||||
|
</input>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setContents(result);
|
||||||
|
|
||||||
|
}
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`ui-form-component ${_config.extraStyles}`}>
|
||||||
|
{ contents }
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Form;
|
||||||
@@ -1,44 +1,25 @@
|
|||||||
import { FC, useCallback, useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { LoggedIn, NotLoggedIn, Registering } from "./variants";
|
import { LoggedIn, NotLoggedIn, Registering } from "./variants";
|
||||||
import { useAuthContext } from "../../../context/AuthContext";
|
import { useAuthContext } from "../../../context/AuthContext";
|
||||||
import { IUser } from "../../../schemas";
|
|
||||||
import "/src/sass/components/Navbar.scss";
|
import "/src/sass/components/Navbar.scss";
|
||||||
|
|
||||||
const Navbar: FC<{receiveChange: (change: IUser) => void}> = ({ receiveChange }) => {
|
const Navbar = () => {
|
||||||
// setup and local state
|
// setup and local state
|
||||||
const navigate = useNavigate();
|
const { user } = useAuthContext();
|
||||||
const { user, setUser } = useAuthContext();
|
const [displayed, setDisplayed] = useState<JSX.Element>(<p>Loading...</p>);
|
||||||
const [received, setReceived] = useState<IUser | undefined>();
|
|
||||||
const [displayed, setDisplayed] = useState<JSX.Element>();
|
|
||||||
|
|
||||||
// lift and store state from navbar variants
|
|
||||||
const liftChange = useCallback((newValue: IUser | undefined) => {
|
|
||||||
if (!newValue) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setUser(newValue);
|
|
||||||
setReceived(newValue);
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const variants = {
|
const variants = {
|
||||||
loggedin: <LoggedIn navigate={navigate} received={received} liftChange={liftChange} />,
|
loggedin: <LoggedIn />,
|
||||||
notloggedin: <NotLoggedIn navigate={navigate} received={received} />,
|
notloggedin: <NotLoggedIn />,
|
||||||
registering: <Registering navigate={navigate} received={received} />
|
registering: <Registering />
|
||||||
}
|
}
|
||||||
|
|
||||||
// side effects for live rendering
|
// side effects for live rendering
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
user && setReceived(user);
|
setDisplayed(user ? variants.loggedin : variants.notloggedin);
|
||||||
}, [user])
|
}, [user]);
|
||||||
|
|
||||||
useEffect(() => {
|
return displayed;
|
||||||
if (received) receiveChange(received);
|
|
||||||
setDisplayed(received ? variants.loggedin : variants.notloggedin);
|
|
||||||
}, [received, setReceived]);
|
|
||||||
|
|
||||||
return displayed || <p>Loading...</p>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Navbar;
|
export default Navbar;
|
||||||
@@ -1,15 +1,26 @@
|
|||||||
import { attemptLogout } from "../../../util/apiUtils";
|
import API from "../../../util/API";
|
||||||
import { NavbarType } from "../../../util/types";
|
import { NavbarType } from "../../../util/types";
|
||||||
import { Button, Dropdown } from '../.'
|
import { Button, Dropdown } from '..'
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { useAuthContext } from "../../../context/AuthContext";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
const LoggedIn = () => {
|
||||||
|
const { user, setUser, setToken } = useAuthContext();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const auth = new API.Auth();
|
||||||
|
|
||||||
const LoggedIn: NavbarType = ({ received, liftChange, navigate }) => {
|
|
||||||
const [dropdownActive, setDropdownActive] = useState(false);
|
const [dropdownActive, setDropdownActive] = useState(false);
|
||||||
const [searchActive, setSearchActive] = useState(false);
|
const [searchActive, setSearchActive] = useState(false);
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
const success = await attemptLogout();
|
const success = await auth.logout();
|
||||||
if (success) liftChange!(undefined);
|
console.log(success);
|
||||||
|
|
||||||
|
// nullify cookie and unset user/token data
|
||||||
|
document.cookie = `token=;expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
||||||
|
setUser(undefined);
|
||||||
|
setToken(undefined);
|
||||||
navigate('/');
|
navigate('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,6 +40,10 @@ const LoggedIn: NavbarType = ({ received, liftChange, navigate }) => {
|
|||||||
navigate(payload);
|
navigate(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(user);
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div id="navbar">
|
<div id="navbar">
|
||||||
@@ -36,7 +51,7 @@ const LoggedIn: NavbarType = ({ received, liftChange, navigate }) => {
|
|||||||
<a onClick={() => navigate('/')}>RECIPIN</a>
|
<a onClick={() => navigate('/')}>RECIPIN</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="navbar-block">
|
<div className="navbar-block">
|
||||||
<p>Hi, {received?.firstname}.</p>
|
<p>Hi, {user && user.firstname}.</p>
|
||||||
<span id="search-icon"></span>
|
<span id="search-icon"></span>
|
||||||
<Button onClick={() => handleUIChange("SEARCH")}>Search</Button>
|
<Button onClick={() => handleUIChange("SEARCH")}>Search</Button>
|
||||||
<Button onClick={() => handleUIChange("ACTIONS")}>Actions</Button>
|
<Button onClick={() => handleUIChange("ACTIONS")}>Actions</Button>
|
||||||
@@ -64,7 +79,9 @@ const LoggedIn: NavbarType = ({ received, liftChange, navigate }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const NotLoggedIn: NavbarType = ({ navigate }) => {
|
const NotLoggedIn = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="navbar">
|
<div id="navbar">
|
||||||
<div className="navbar-block">
|
<div className="navbar-block">
|
||||||
@@ -77,14 +94,17 @@ const NotLoggedIn: NavbarType = ({ navigate }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Registering: NavbarType = ({ received, navigate }) => {
|
const Registering = () => {
|
||||||
|
const { user } = useAuthContext();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="navbar">
|
<div id="navbar">
|
||||||
<div className="navbar-block">
|
<div className="navbar-block">
|
||||||
<a onClick={() => navigate('/')}>RECIPIN</a>
|
<a onClick={() => navigate('/')}>RECIPIN</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="navbar-block">
|
<div className="navbar-block">
|
||||||
<p>Hi, {received?.firstname}.</p>
|
<p>Hi, {user?.firstname}.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,8 +3,12 @@ import { IUser } from "../../../schemas";
|
|||||||
import { TextField, UserCard } from "..";
|
import { TextField, UserCard } from "..";
|
||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
import { getAllUsers } from "../../../util/apiUtils";
|
import { getAllUsers } from "../../../util/apiUtils";
|
||||||
|
import API from "../../../util/API";
|
||||||
|
import { useAuthContext } from "../../../context/AuthContext";
|
||||||
|
|
||||||
const FriendSearchWidget: FC<{}> = () => {
|
const FriendSearchWidget: FC<{}> = () => {
|
||||||
|
const { token } = useAuthContext();
|
||||||
|
|
||||||
const [searchTerm, setSearchTerm] = useState<string>();
|
const [searchTerm, setSearchTerm] = useState<string>();
|
||||||
const [userPool, setUserPool] = useState<IUser[]>([]);
|
const [userPool, setUserPool] = useState<IUser[]>([]);
|
||||||
const [friendResults, setFriendResults] = useState<IUser[]>([]);
|
const [friendResults, setFriendResults] = useState<IUser[]>([]);
|
||||||
@@ -23,7 +27,10 @@ const FriendSearchWidget: FC<{}> = () => {
|
|||||||
// load available user pool on mount
|
// load available user pool on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async function() {
|
(async function() {
|
||||||
const result = await getAllUsers();
|
if (!token) return;
|
||||||
|
const users = new API.User(token);
|
||||||
|
|
||||||
|
const result = await users.getAll();
|
||||||
if (result) setUserPool(result);
|
if (result) setUserPool(result);
|
||||||
})();
|
})();
|
||||||
}, [])
|
}, [])
|
||||||
|
|||||||
@@ -2,13 +2,17 @@ import { createContext, Dispatch, SetStateAction, useContext } from "react";
|
|||||||
import { IUser } from "../schemas";
|
import { IUser } from "../schemas";
|
||||||
|
|
||||||
export interface IAuthContext {
|
export interface IAuthContext {
|
||||||
user?: IUser
|
user: IUser | undefined
|
||||||
setUser: Dispatch<SetStateAction<IUser>> | VoidFunction
|
setUser: Dispatch<SetStateAction<IUser | undefined>> | VoidFunction
|
||||||
|
token: string | undefined
|
||||||
|
setToken: Dispatch<SetStateAction<string | undefined>> | VoidFunction
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultValue: IAuthContext = {
|
export const defaultValue: IAuthContext = {
|
||||||
user: undefined,
|
user: undefined,
|
||||||
setUser: () => {}
|
setUser: () => {},
|
||||||
|
token: undefined,
|
||||||
|
setToken: () => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuthContext = createContext<IAuthContext>(defaultValue);
|
export const AuthContext = createContext<IAuthContext>(defaultValue);
|
||||||
|
|||||||
18
client/src/context/AuthProvider.tsx
Normal file
18
client/src/context/AuthProvider.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { FC, Provider, ReactPortal, useEffect, useState } from "react"
|
||||||
|
import { IUser } from "../schemas";
|
||||||
|
import { AuthContext, IAuthContext } from "./AuthContext";
|
||||||
|
|
||||||
|
const AuthProvider = ({ children }: any) => {
|
||||||
|
const [user, setUser] = useState<IUser>();
|
||||||
|
const [token, setToken] = useState<string>();
|
||||||
|
|
||||||
|
const value = { user, setUser, token, setToken }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider value={ value }>
|
||||||
|
{ children }
|
||||||
|
</AuthContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuthProvider;
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
|
import AuthProvider from './context/AuthProvider'
|
||||||
import './sass/index.scss'
|
import './sass/index.scss'
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<App />)
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
|
<AuthProvider>
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
</AuthProvider>
|
||||||
|
)
|
||||||
|
|||||||
156
client/src/util/API.ts
Normal file
156
client/src/util/API.ts
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import { AxiosHeaders, AxiosRequestHeaders } from "axios";
|
||||||
|
import { IUser, IUserAuth, IFriendship, IRecipe, IIngredient, ICollection, IGroceryList } from "../schemas";
|
||||||
|
import { default as _instance } from "./axiosInstance";
|
||||||
|
|
||||||
|
module API {
|
||||||
|
export class Settings {
|
||||||
|
private static APISTRING = import.meta.env.APISTRING || "http://localhost:8080";
|
||||||
|
private static token?: string;
|
||||||
|
|
||||||
|
public static getAPISTRING() {
|
||||||
|
return Settings.APISTRING;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getToken() {
|
||||||
|
return Settings.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static setToken(newToken: string) {
|
||||||
|
Settings.token = newToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class RestController<T> {
|
||||||
|
protected instance = _instance;
|
||||||
|
protected endpoint: string;
|
||||||
|
protected headers?: any
|
||||||
|
|
||||||
|
constructor(endpoint: string, token: string) {
|
||||||
|
this.endpoint = endpoint;
|
||||||
|
this.headers = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": ("Bearer " + token)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAll() {
|
||||||
|
const response = await this.instance.get(this.endpoint, this.headers);
|
||||||
|
return Promise.resolve(response.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getByID(id: string) {
|
||||||
|
const response = await this.instance.get(this.endpoint + "/" + id, this.headers);
|
||||||
|
return Promise.resolve(response.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async post(data: T) {
|
||||||
|
console.log(data);
|
||||||
|
const response = await this.instance.post(this.endpoint, data, this.headers);
|
||||||
|
return Promise.resolve(response.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async put(id: string, data: T | Partial<T>) {
|
||||||
|
const response = await this.instance.put(this.endpoint + "/" + id, JSON.stringify(data), this.headers);
|
||||||
|
return Promise.resolve(response.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string) {
|
||||||
|
const response = await this.instance.delete(this.endpoint + '/' + id, this.headers);
|
||||||
|
return Promise.resolve(response.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Auth {
|
||||||
|
private instance = _instance;
|
||||||
|
private endpoint = Settings.getAPISTRING() + "/auth";
|
||||||
|
|
||||||
|
async login(data: IUserAuth | Partial<IUser>) {
|
||||||
|
try {
|
||||||
|
const response = await this.instance.post(this.endpoint + "/login", data);
|
||||||
|
return Promise.resolve(response.data);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async register(data: IUser) {
|
||||||
|
try {
|
||||||
|
const response = await this.instance.post(this.endpoint + "/register", data);
|
||||||
|
return Promise.resolve(response.data);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async logout() {
|
||||||
|
try {
|
||||||
|
const response = await this.instance.delete(this.endpoint + '/logout');
|
||||||
|
|
||||||
|
// unset cookie data and send response
|
||||||
|
document.cookie = `token=;expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
||||||
|
return Promise.resolve(response.data);
|
||||||
|
} catch(err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// for future use
|
||||||
|
async registerGoogle() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loginGoogle() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async logoutGoogle() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class User extends RestController<IUser> {
|
||||||
|
constructor(token: string) {
|
||||||
|
super(Settings.getAPISTRING() + "/app/users", token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Friendship extends RestController<IFriendship> {
|
||||||
|
constructor(token: string) {
|
||||||
|
super(Settings.getAPISTRING() + "/app/friends", token);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPendingFriendRequests() {
|
||||||
|
const response = await this.instance.get(this.endpoint + "?pending=true", this.headers);
|
||||||
|
return Promise.resolve(response.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Recipe extends RestController<IRecipe> {
|
||||||
|
constructor(token: string) {
|
||||||
|
super(Settings.getAPISTRING() + "/app/recipes", token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Ingredient extends RestController<IIngredient> {
|
||||||
|
constructor(token: string) {
|
||||||
|
if (!token) throw new Error("Missing required token");
|
||||||
|
super(Settings.getAPISTRING() + "/app/ingredients", token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Collection extends RestController<ICollection> {
|
||||||
|
constructor(token: string) {
|
||||||
|
super(Settings.getAPISTRING() + "/app/collection", token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GroceryList extends RestController<IGroceryList> {
|
||||||
|
constructor(token: string) {
|
||||||
|
super(Settings.getAPISTRING() + "/app/grocery-list", token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default API
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ICollection, IUser, IUserAuth } from "../schemas";
|
import { ICollection, IUser, IUserAuth } from "../schemas";
|
||||||
// import { IAuthContext } from "../context/AuthContext";
|
import instance from "./axiosInstance";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
const API = import.meta.env.APISTRING || "http://localhost:8080";
|
const API = import.meta.env.APISTRING || "http://localhost:8080";
|
||||||
|
|
||||||
|
|||||||
20
client/src/util/axiosInstance.ts
Normal file
20
client/src/util/axiosInstance.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import axios, { AxiosResponse } from 'axios'
|
||||||
|
import jwt_decode from 'jwt-decode'
|
||||||
|
|
||||||
|
const apiUrl = import.meta.env.VITE_APIURL;
|
||||||
|
|
||||||
|
const instance = axios.create({
|
||||||
|
baseURL: apiUrl
|
||||||
|
});
|
||||||
|
|
||||||
|
instance.interceptors.response.use((res: AxiosResponse<any,any>) => {
|
||||||
|
if (res?.data.token) {
|
||||||
|
document.cookie = `token=${res.data.token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}, (err) => {
|
||||||
|
return Promise.reject(err);
|
||||||
|
})
|
||||||
|
|
||||||
|
export default instance;
|
||||||
19
client/src/util/handleToken.ts
Normal file
19
client/src/util/handleToken.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import jwtDecode from "jwt-decode";
|
||||||
|
import { IUser } from "../schemas";
|
||||||
|
import { TokenType } from "./types";
|
||||||
|
|
||||||
|
export default function handleToken(): { token: string, user: IUser } | null {
|
||||||
|
try {
|
||||||
|
const extractedToken: Partial<TokenType> = jwtDecode(document.cookie.split("=")[1]);
|
||||||
|
if (extractedToken) {
|
||||||
|
return {
|
||||||
|
token: document.cookie.split("=")[1],
|
||||||
|
user: extractedToken.user as IUser
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(e: any) {
|
||||||
|
console.log(e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -47,3 +47,7 @@ export type ProtectPortal = FC<ProtectParams>
|
|||||||
export type UserCardType = FC<UserCardProps>
|
export type UserCardType = FC<UserCardProps>
|
||||||
export type NavbarType = FC<NavbarProps>
|
export type NavbarType = FC<NavbarProps>
|
||||||
export type CheckboxType = FC<CheckboxProps>
|
export type CheckboxType = FC<CheckboxProps>
|
||||||
|
|
||||||
|
export interface TokenType {
|
||||||
|
user: IUser
|
||||||
|
}
|
||||||
@@ -20,12 +20,25 @@ export default class AuthService {
|
|||||||
// not allowed to use email address that already exists
|
// not allowed to use email address that already exists
|
||||||
const user = await UserInstance.getOneByEmail(data.email);
|
const user = await UserInstance.getOneByEmail(data.email);
|
||||||
|
|
||||||
if (user) throw createError('409', 'Email already in use');
|
if (user) {
|
||||||
|
return new ControllerResponse(StatusCode.Conflict, "Email already in use", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that all required fields are populated
|
||||||
|
let missingFields = new Array<string>();
|
||||||
|
let requiredFields: Array<keyof IUser> = ['firstname', 'lastname', 'handle', 'email', 'isadmin', 'password'];
|
||||||
|
for (let field of requiredFields) {
|
||||||
|
if (!(field in data)) {
|
||||||
|
missingFields.push(field as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missingFields.length) {
|
||||||
|
return new ControllerResponse(StatusCode.BadRequest, `Missing fields in output: ${missingFields.join(", ")}`, false);
|
||||||
|
}
|
||||||
|
|
||||||
// hash password and create new user record
|
// hash password and create new user record
|
||||||
const salt = await bcrypt.genSalt(12);
|
const salt = await bcrypt.genSalt(12);
|
||||||
console.log(salt);
|
|
||||||
console.log(data.password);
|
|
||||||
|
|
||||||
bcrypt.hash(data.password!, salt, (err, hash) => {
|
bcrypt.hash(data.password!, salt, (err, hash) => {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
@@ -37,7 +50,7 @@ export default class AuthService {
|
|||||||
UserInstance.post(newData);
|
UserInstance.post(newData);
|
||||||
})
|
})
|
||||||
|
|
||||||
return true;
|
return new ControllerResponse(StatusCode.NewContent, "registered successfully", true);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
throw new Error(e);
|
throw new Error(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,23 @@
|
|||||||
import e, { NextFunction, Request, Response } from "express"
|
import { NextFunction, Request, Response } from "express"
|
||||||
import ControllerResponse from "../util/ControllerResponse";
|
import dotenv from "dotenv";
|
||||||
import { StatusCode } from "../util/types";
|
import { IUser } from "../schemas";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
export function restrictAccess(req: Request, res: Response, next: NextFunction) {
|
export function restrictAccess(req: Request, res: Response, next: NextFunction) {
|
||||||
if (req.session.user == undefined) {
|
if (req.session.user == undefined) {
|
||||||
console.log("restricted")
|
res.send("content restricted");
|
||||||
res.send(undefined);
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function requireSessionSecret(req: Request, res: Response, next: NextFunction) {
|
||||||
|
const secret = process.env.SESSIONSECRET;
|
||||||
|
|
||||||
|
if (!secret) {
|
||||||
|
res.sendStatus(500);
|
||||||
|
throw new Error("Express secret is undefined");
|
||||||
} else {
|
} else {
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
@@ -20,5 +32,11 @@ export function checkFriendStatus(req: Request, res: Response, next: NextFunctio
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function checkIsAdmin(req: Request, res: Response, next: NextFunction) {
|
export function checkIsAdmin(req: Request, res: Response, next: NextFunction) {
|
||||||
|
const user: IUser | undefined = req.user as IUser;
|
||||||
|
|
||||||
|
if (user.isadmin) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
res.status(403).send("Unauthorized");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import cors from 'cors';
|
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import { loaders } from './loaders';
|
import { loaders } from './loaders';
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
const port = 8080;
|
const port = process.env.PORT || 8080;
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(cors());
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
await loaders(app);
|
await loaders(app);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Express } from 'express';
|
import express, { Express } from 'express';
|
||||||
import bodyParser from 'body-parser';
|
import bodyParser from 'body-parser';
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
import morgan from 'morgan';
|
import morgan from 'morgan';
|
||||||
@@ -6,39 +6,31 @@ import cors from 'cors';
|
|||||||
import session from 'express-session';
|
import session from 'express-session';
|
||||||
import pgSessionStore from '../db/sessionStore';
|
import pgSessionStore from '../db/sessionStore';
|
||||||
import { IUser } from '../schemas';
|
import { IUser } from '../schemas';
|
||||||
|
import { requireSessionSecret } from '../auth/middlewares';
|
||||||
|
|
||||||
declare module "express-session" {
|
const origin = process.env.ORIGIN || 'http://localhost:5173';
|
||||||
|
const secret = process.env.SESSIONSECRET;
|
||||||
|
|
||||||
|
declare module 'express-session' {
|
||||||
interface SessionData {
|
interface SessionData {
|
||||||
user: IUser
|
user?: IUser
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const expressLoader = async (app: Express) => {
|
export const expressLoader = async (app: Express) => {
|
||||||
app.use(cors({
|
app.use(cors({ origin: origin }));
|
||||||
origin: process.env.ORIGIN || 'http://localhost:5173',
|
app.use(express.json());
|
||||||
credentials: true
|
app.use(express.urlencoded({ extended: true }));
|
||||||
}));
|
|
||||||
|
|
||||||
app.use(bodyParser.json());
|
|
||||||
app.use(bodyParser.urlencoded({ extended: true }));
|
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
|
|
||||||
// app.options("*", cors({ origin: 'http://localhost:5173', optionsSuccessStatus: 200 }));
|
|
||||||
// app.use(cors({ origin: "http://localhost:5173", optionsSuccessStatus: 200 }));
|
|
||||||
|
|
||||||
app.use(morgan('tiny'));
|
app.use(morgan('tiny'));
|
||||||
|
app.use(requireSessionSecret);
|
||||||
app.get('/', (req, res) => {
|
|
||||||
res.cookie('name', 'express').send('cookie set');
|
|
||||||
})
|
|
||||||
|
|
||||||
const secret = process.env.SESSIONSECRET as string;
|
|
||||||
|
|
||||||
app.use(session({
|
app.use(session({
|
||||||
secret: secret,
|
secret: secret as string,
|
||||||
cookie: {
|
cookie: {
|
||||||
maxAge: 8 * 60 * 60 * 1000,
|
maxAge: 8 * 60 * 60 * 1000,
|
||||||
secure: false
|
secure: false,
|
||||||
|
httpOnly: false
|
||||||
},
|
},
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: false,
|
saveUninitialized: false,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { passportLoader } from './passport';
|
|||||||
|
|
||||||
export const loaders = async (app: Express) => {
|
export const loaders = async (app: Express) => {
|
||||||
const expressApp = await expressLoader(app);
|
const expressApp = await expressLoader(app);
|
||||||
const passportApp = await passportLoader(expressApp);
|
await passportLoader(expressApp);
|
||||||
await swaggerLoader(expressApp);
|
await swaggerLoader(expressApp);
|
||||||
await routes(expressApp, passportApp);
|
await routes(expressApp);
|
||||||
}
|
}
|
||||||
@@ -1,32 +1,35 @@
|
|||||||
import { Strategy as LocalStrategy } from "passport-local";
|
|
||||||
import passport from "passport";
|
import passport from "passport";
|
||||||
import { Express } from "express";
|
import { Express } from "express";
|
||||||
import AuthService from "../auth";
|
import { ExtractJwt, Strategy as JwtStrategy } from "passport-jwt";
|
||||||
import { IUserAuth } from "../schemas";
|
|
||||||
const AuthInstance = new AuthService();
|
|
||||||
|
|
||||||
export const passportLoader = async (app: Express) => {
|
export const passportLoader = async (app: Express) => {
|
||||||
app.use(passport.initialize());
|
app.use(passport.initialize());
|
||||||
app.use(passport.session());
|
app.use(passport.session());
|
||||||
|
|
||||||
passport.serializeUser((user, done) => {
|
passport.serializeUser((user: Express.User, done) => {
|
||||||
done(null, user);
|
process.nextTick(() => {
|
||||||
|
done(null, user);
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
passport.deserializeUser((user: IUserAuth, done) => {
|
passport.deserializeUser((user: Express.User, done) => {
|
||||||
done(null, user);
|
process.nextTick(() => {
|
||||||
|
done(null, user);
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// sign in method with passport local strategy
|
// config for jwt strategy
|
||||||
passport.use(new LocalStrategy({
|
let opts = {
|
||||||
usernameField: 'email',
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
passwordField: 'password'
|
secretOrKey: 'secret'
|
||||||
}, async (email, password, done) => {
|
}
|
||||||
|
|
||||||
|
// jwt strategy
|
||||||
|
passport.use(new JwtStrategy(opts, async (token, done) => {
|
||||||
try {
|
try {
|
||||||
const response = await AuthInstance.login({ email, password });
|
return done(null, token.user);
|
||||||
return done(null, response);
|
} catch (error) {
|
||||||
} catch (e: any) {
|
done(error);
|
||||||
return done(e);
|
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "bash util/build.sh",
|
"build": "bash util/build.sh",
|
||||||
"seed": "npm run build && ts-node-dev db/seed.ts",
|
"seed": "npm run build && ts-node --files db/seed.ts",
|
||||||
"dev": "bash util/dev.sh",
|
"dev": "bash util/dev.sh",
|
||||||
"prod": "npm run build && node dist/index.js",
|
"prod": "npm run build && node dist/index.js",
|
||||||
"test": "jest --coverage",
|
"test": "jest --coverage",
|
||||||
@@ -15,7 +15,6 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/cookie-parser": "^1.4.3",
|
|
||||||
"bcrypt": "^5.1.0",
|
"bcrypt": "^5.1.0",
|
||||||
"body-parser": "^1.20.1",
|
"body-parser": "^1.20.1",
|
||||||
"connect-pg-simple": "^8.0.0",
|
"connect-pg-simple": "^8.0.0",
|
||||||
@@ -28,8 +27,10 @@
|
|||||||
"helmet": "^6.0.0",
|
"helmet": "^6.0.0",
|
||||||
"http-errors": "^2.0.0",
|
"http-errors": "^2.0.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
|
"jsonwebtoken": "^9.0.0",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
|
"passport-jwt": "^4.0.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"pg": "^8.8.0",
|
"pg": "^8.8.0",
|
||||||
"pg-promise": "^10.15.0",
|
"pg-promise": "^10.15.0",
|
||||||
@@ -38,16 +39,18 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "^5.0.0",
|
||||||
"@types/connect-pg-simple": "^7.0.0",
|
"@types/connect-pg-simple": "^7.0.0",
|
||||||
|
"@types/cookie-parser": "^1.4.3",
|
||||||
"@types/cors": "^2.8.12",
|
"@types/cors": "^2.8.12",
|
||||||
"@types/dotenv": "^8.2.0",
|
"@types/dotenv": "^8.2.0",
|
||||||
"@types/express": "^4.17.14",
|
"@types/express": "^4.17.14",
|
||||||
"@types/express-session": "^1.17.5",
|
"@types/express-session": "^1.17.6",
|
||||||
"@types/http-errors": "^2.0.1",
|
"@types/http-errors": "^2.0.1",
|
||||||
"@types/jest": "^29.2.4",
|
"@types/jest": "^29.2.4",
|
||||||
"@types/js-yaml": "^4.0.5",
|
"@types/js-yaml": "^4.0.5",
|
||||||
"@types/morgan": "^1.9.3",
|
"@types/morgan": "^1.9.3",
|
||||||
"@types/node": "^18.11.9",
|
"@types/node": "^18.11.9",
|
||||||
"@types/passport": "^1.0.11",
|
"@types/passport": "^1.0.11",
|
||||||
|
"@types/passport-jwt": "^3.0.8",
|
||||||
"@types/passport-local": "^1.0.34",
|
"@types/passport-local": "^1.0.34",
|
||||||
"@types/pg": "^8.6.5",
|
"@types/pg": "^8.6.5",
|
||||||
"@types/pg-promise": "^5.4.3",
|
"@types/pg-promise": "^5.4.3",
|
||||||
@@ -57,6 +60,7 @@
|
|||||||
"nodemon": "^2.0.20",
|
"nodemon": "^2.0.20",
|
||||||
"supertest": "^6.3.3",
|
"supertest": "^6.3.3",
|
||||||
"ts-jest": "^29.0.3",
|
"ts-jest": "^29.0.3",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
"ts-node-dev": "^2.0.0",
|
"ts-node-dev": "^2.0.0",
|
||||||
"tslint": "^6.1.3",
|
"tslint": "^6.1.3",
|
||||||
"typescript": "^4.9.3"
|
"typescript": "^4.9.3"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Express, Request, Router } from "express"
|
import { Express, Router } from "express"
|
||||||
import { PassportStatic } from "passport";
|
import { PassportStatic } from "passport";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
import { IUser, IUserAuth } from "../schemas";
|
import { IUser, IUserAuth } from "../schemas";
|
||||||
import AuthService from "../auth";
|
import AuthService from "../auth";
|
||||||
import { UserCtl } from "../controllers";
|
import { UserCtl } from "../controllers";
|
||||||
@@ -12,18 +13,9 @@ const UserInstance = new UserCtl();
|
|||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
export const authRoute = (app: Express, passport: PassportStatic) => {
|
export const authRoute = (app: Express) => {
|
||||||
app.use('/auth', router);
|
app.use('/auth', router);
|
||||||
|
|
||||||
// router.use((req, res, next) => {
|
|
||||||
// console.log(req.isAuthenticated());
|
|
||||||
// console.log(req.session.user);
|
|
||||||
// console.log(req.cookies);
|
|
||||||
// console.log();
|
|
||||||
|
|
||||||
// next();
|
|
||||||
// })
|
|
||||||
|
|
||||||
router.use((req, res, next) => {
|
router.use((req, res, next) => {
|
||||||
console.log(req.session);
|
console.log(req.session);
|
||||||
next();
|
next();
|
||||||
@@ -49,7 +41,7 @@ export const authRoute = (app: Express, passport: PassportStatic) => {
|
|||||||
res.status(200).send({ message: "Cool restricted content!" });
|
res.status(200).send({ message: "Cool restricted content!" });
|
||||||
})
|
})
|
||||||
|
|
||||||
router.post('/login', passport.authenticate('local'), async (req, res, next) => {
|
router.post('/login', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const data: IUserAuth = req.body;
|
const data: IUserAuth = req.body;
|
||||||
console.log(data);
|
console.log(data);
|
||||||
@@ -59,19 +51,29 @@ export const authRoute = (app: Express, passport: PassportStatic) => {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const user = response.data as IUser;
|
const user = response.data as IUser;
|
||||||
|
|
||||||
req.session.regenerate((err) => {
|
req.user = user;
|
||||||
if (err) next(err);
|
req.session.user = user;
|
||||||
req.session.user = user;
|
|
||||||
|
|
||||||
req.session.save((err) => {
|
const safeUserData = {
|
||||||
if (err) return next(err);
|
id: user.id,
|
||||||
})
|
firstname: user.firstname,
|
||||||
|
lastname: user.lastname,
|
||||||
|
handle: user.handle,
|
||||||
|
email: user.email,
|
||||||
|
datecreated: user.datecreated,
|
||||||
|
datemodified: user.datemodified
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = jwt.sign({ user: safeUserData }, process.env.SESSIONSECRET as string);
|
||||||
|
|
||||||
|
req.session.save((err) => {
|
||||||
|
return next(err);
|
||||||
})
|
})
|
||||||
|
|
||||||
res.cookie('userid', user.id, { maxAge: 1000 * 60 * 60 * 24 });
|
console.log(req.session);
|
||||||
|
|
||||||
res.send(response);
|
res.cookie('token', token, { httpOnly: true });
|
||||||
res.end();
|
res.json({ token });
|
||||||
} else {
|
} else {
|
||||||
res.status(401).send({ message: "Login unsuccessful" });
|
res.status(401).send({ message: "Login unsuccessful" });
|
||||||
}
|
}
|
||||||
@@ -82,10 +84,11 @@ export const authRoute = (app: Express, passport: PassportStatic) => {
|
|||||||
|
|
||||||
router.post('/register', async (req, res, next) => {
|
router.post('/register', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const data = req.body;
|
const data: IUser = req.body;
|
||||||
const response = await AuthInstance.register(data);
|
const response = await AuthInstance.register(data);
|
||||||
if (!response) res.status(400).send({ ok: false });
|
response.represent();
|
||||||
res.status(200).send({ ok: true });
|
|
||||||
|
res.status(response.code).send({ ok: response.ok, message: response.data });
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
next(e);
|
next(e);
|
||||||
}
|
}
|
||||||
@@ -93,11 +96,9 @@ export const authRoute = (app: Express, passport: PassportStatic) => {
|
|||||||
|
|
||||||
router.delete('/logout', async (req, res, next) => {
|
router.delete('/logout', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
req.session.destroy((err) => {
|
res.clearCookie('connect.sid').clearCookie('token');
|
||||||
if (err) throw err;
|
res.status(204).send("logout successful");
|
||||||
})
|
res.end();
|
||||||
res.clearCookie('userid');
|
|
||||||
res.status(204).send({ ok: true });
|
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
next(e);
|
next(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
import { Express, Router } from "express";
|
import { Express, Router } from "express";
|
||||||
import { restrictAccess } from "../auth/middlewares";
|
import { checkIsAdmin, restrictAccess } from "../auth/middlewares";
|
||||||
import CollectionCtl from "../controllers/CollectionCtl";
|
import CollectionCtl from "../controllers/CollectionCtl";
|
||||||
const CollectionInstance = new CollectionCtl();
|
const CollectionInstance = new CollectionCtl();
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
export const collectionRoute = (app: Express) => {
|
export const collectionRoute = (app: Express) => {
|
||||||
app.use('/collection', router);
|
app.use('/app/collection', router);
|
||||||
|
|
||||||
router.get('/:id', restrictAccess, async (req, res, next) => {
|
router.use((req, res, next) => {
|
||||||
|
console.log('what gives');
|
||||||
|
console.log(req.body);
|
||||||
|
next();
|
||||||
|
})
|
||||||
|
|
||||||
|
router.get('/:id', async (req, res, next) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
try {
|
try {
|
||||||
const { code, data } = await CollectionInstance.getOne(id);
|
const { code, data } = await CollectionInstance.getOne(id);
|
||||||
@@ -19,7 +25,7 @@ export const collectionRoute = (app: Express) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// implement is admin on this route
|
// implement is admin on this route
|
||||||
router.get('/', restrictAccess, async (req, res, next) => {
|
router.get('/', checkIsAdmin, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { code, data } = await CollectionInstance.getAll();
|
const { code, data } = await CollectionInstance.getAll();
|
||||||
res.status(code).send(data);
|
res.status(code).send(data);
|
||||||
@@ -28,9 +34,9 @@ export const collectionRoute = (app: Express) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
router.post('/', restrictAccess, async (req, res, next) => {
|
router.post('/', async (req, res, next) => {
|
||||||
const data = req.body;
|
const data = req.body;
|
||||||
console.log(data);
|
console.log(req.body ?? "sanity check");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await CollectionInstance.post(data);
|
const result = await CollectionInstance.post(data);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const CourseInstance = new CourseCtl();
|
|||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
export const courseRouter = (app: Express) => {
|
export const courseRouter = (app: Express) => {
|
||||||
app.use('/course', router);
|
app.use('/app/course', router);
|
||||||
|
|
||||||
router.get('/', async (req, res, next) => {
|
router.get('/', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const UserInstance = new UserCtl();
|
|||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
export const friendRouter = (app: Express) => {
|
export const friendRouter = (app: Express) => {
|
||||||
app.use('/friend', router);
|
app.use('/app/friend', router);
|
||||||
|
|
||||||
router.use((req, res, next) => {
|
router.use((req, res, next) => {
|
||||||
let test = req.session.user;
|
let test = req.session.user;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const groceryinstance = new GroceryListCtl();
|
|||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
export const groceryListRoute = (app: Express) => {
|
export const groceryListRoute = (app: Express) => {
|
||||||
app.use('/grocery-list', router);
|
app.use('/app/grocery-list', router);
|
||||||
|
|
||||||
router.get('/', async (req, res, next) => {
|
router.get('/', async (req, res, next) => {
|
||||||
const userid = req.query.userid as string;
|
const userid = req.query.userid as string;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import dotenv from 'dotenv';
|
||||||
import { Express } from "express"
|
import { Express } from "express"
|
||||||
import { PassportStatic } from "passport";
|
|
||||||
import { userRoute } from "./users";
|
import { userRoute } from "./users";
|
||||||
import { recipeRoute } from "./recipe";
|
import { recipeRoute } from "./recipe";
|
||||||
import { collectionRoute } from "./collection";
|
import { collectionRoute } from "./collection";
|
||||||
@@ -11,14 +12,39 @@ import { friendRouter } from "./friend";
|
|||||||
import { cuisineRouter } from "./cuisine";
|
import { cuisineRouter } from "./cuisine";
|
||||||
import { courseRouter } from "./course";
|
import { courseRouter } from "./course";
|
||||||
|
|
||||||
export const routes = async (app: Express, passport: PassportStatic) => {
|
dotenv.config();
|
||||||
|
|
||||||
|
export const routes = async (app: Express) => {
|
||||||
|
// unprotected routes
|
||||||
|
authRoute(app);
|
||||||
|
|
||||||
|
// middleware to check for auth on cookies on each request in protected routes
|
||||||
|
app.use('/app', async (req, res, next) => {
|
||||||
|
// pull jwt from request headers
|
||||||
|
console.log(req.headers);
|
||||||
|
const token = req.headers['authorization']?.split(" ")[1];
|
||||||
|
console.log(token);
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
res.status(403).send("Unauthorized, did not receive token");
|
||||||
|
} else {
|
||||||
|
jwt.verify(token, process.env.SESSIONSECRET as string, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
res.status(403).send(err);
|
||||||
|
} else {
|
||||||
|
console.log(data);
|
||||||
|
req.user = data;
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// protected routes
|
||||||
userRoute(app);
|
userRoute(app);
|
||||||
friendRouter(app);
|
friendRouter(app);
|
||||||
recipeRoute(app);
|
recipeRoute(app);
|
||||||
ingredientRoute(app);
|
ingredientRoute(app);
|
||||||
|
|
||||||
// to do: refactor for ctlresponse
|
|
||||||
authRoute(app, passport);
|
|
||||||
collectionRoute(app);
|
collectionRoute(app);
|
||||||
subscriptionRoute(app);
|
subscriptionRoute(app);
|
||||||
groceryListRoute(app);
|
groceryListRoute(app);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const IngredientInstance = new IngredientCtl();
|
|||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
export const ingredientRoute = (app: Express) => {
|
export const ingredientRoute = (app: Express) => {
|
||||||
app.use('/ingredient', router);
|
app.use('/app/ingredient', router);
|
||||||
|
|
||||||
router.get('/', async (req, res, next) => {
|
router.get('/', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const recipectl = new RecipeCtl();
|
|||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
export const recipeRoute = (app: Express) => {
|
export const recipeRoute = (app: Express) => {
|
||||||
app.use('/recipe', router);
|
app.use('/app/recipe', router);
|
||||||
|
|
||||||
router.get('/:id', async (req, res, next) => {
|
router.get('/:id', async (req, res, next) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const CollectionInstance = new CollectionCtl();
|
|||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
export const subscriptionRoute = (app: Express) => {
|
export const subscriptionRoute = (app: Express) => {
|
||||||
app.use('/subscription', router);
|
app.use('/app/subscription', router);
|
||||||
|
|
||||||
router.get('/', async (req, res, next) => {
|
router.get('/', async (req, res, next) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const router = Router();
|
|||||||
const userCtl = new UserCtl();
|
const userCtl = new UserCtl();
|
||||||
|
|
||||||
export const userRoute = (app: Express) => {
|
export const userRoute = (app: Express) => {
|
||||||
app.use('/users', router);
|
app.use('/app/users', router);
|
||||||
|
|
||||||
// get all users
|
// get all users
|
||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export interface IUser extends HasHistory, CanDeactivate {
|
|||||||
handle: string
|
handle: string
|
||||||
email: string
|
email: string
|
||||||
isadmin: boolean
|
isadmin: boolean
|
||||||
password?: string
|
password: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUserAuth {
|
export interface IUserAuth {
|
||||||
|
|||||||
@@ -8,10 +8,14 @@ export default class ControllerResponse<T> implements CtlResponse<T> {
|
|||||||
constructor(code: StatusCode, data: T | string, ok?: boolean) {
|
constructor(code: StatusCode, data: T | string, ok?: boolean) {
|
||||||
this.code = code
|
this.code = code
|
||||||
this.data = data
|
this.data = data
|
||||||
this.ok = ok || (this.data !== null)
|
this.ok = ok ?? (this.data !== null)
|
||||||
}
|
}
|
||||||
|
|
||||||
send() {
|
send() {
|
||||||
return { ok: this.ok, code: this.code, data: this.data }
|
return { ok: this.ok, code: this.code, data: this.data }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
represent() {
|
||||||
|
console.log({ ok: this.ok, code: this.code, data: this.data });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
#! /bin/bash
|
#! /bin/bash
|
||||||
|
|
||||||
rm -rf dist && mkdir -p dist && cp ./swagger.yaml ./dist && ./node_modules/.bin/tsc --project ./tsconfig.json --watch & ts-node-dev index.ts
|
rm -rf dist && mkdir -p dist && cp ./swagger.yaml ./dist && ./node_modules/.bin/tsc --project ./tsconfig.json --watch & ts-node --files index.ts
|
||||||
|
|||||||
@@ -13,5 +13,6 @@ export enum StatusCode {
|
|||||||
Unauthorized = 401,
|
Unauthorized = 401,
|
||||||
Forbidden = 403,
|
Forbidden = 403,
|
||||||
NotFound = 404,
|
NotFound = 404,
|
||||||
|
Conflict = 409,
|
||||||
ServerError = 500
|
ServerError = 500
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user