Compare commits

...
This repository has been archived on 2025-11-04. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.

15 commits

121 changed files with 46166 additions and 14531 deletions

7
.eslintignore Normal file
View file

@ -0,0 +1,7 @@
*.json
*.yml
*.sqlite3
*.md
**/public
LICENSE
Dockerfile

15
.eslintrc.yml Normal file
View file

@ -0,0 +1,15 @@
env:
browser: true
es2021: true
jest: true
extends:
- plugin:react/recommended
- airbnb
overrides: []
parserOptions:
ecmaVersion: latest
sourceType: module
plugins:
- react
- jest
rules: { 'indent': ['error', 4, { 'SwitchCase': 1 }], 'comma-dangle': 'off' }

View file

@ -10,7 +10,7 @@ on:
workflow_dispatch:
jobs:
test:
test-build:
environment: test
runs-on: ubuntu-latest
@ -28,3 +28,17 @@ jobs:
run: npm ci
- name: Test build
run: npm run test
run-eslint:
environment: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 16.x
uses: actions/setup-node@v2
with:
node-version: 16.x
- name: Install dependencies
run: npm ci
- name: Test formmating
run: npm run eslint:all

22
.gitignore vendored
View file

@ -1,17 +1,9 @@
# Created by https://www.toptal.com/developers/gitignore/api/node,react,git,visualstudiocode
# Edit at https://www.toptal.com/developers/gitignore?templates=node,react,git,visualstudiocode
**/dist.tar.gz
### Express ###
**/public/
### TODO ###
TODO
### Sqlite ###
server/**/public
*.sqlite3
*.sqlite3-journal
# Created by https://www.toptal.com/developers/gitignore/api/node,react,visualstudiocode,git
# Edit at https://www.toptal.com/developers/gitignore?templates=node,react,visualstudiocode,git
### Git ###
# Created by git for backups. To disable backups in Git:
@ -200,6 +192,4 @@ sketch
.history
.ionide
# Support for Project snippet scope
# End of https://www.toptal.com/developers/gitignore/api/node,react,git,visualstudiocode
# End of https://www.toptal.com/developers/gitignore/api/node,react,visualstudiocode,git

1
.npmrc Normal file
View file

@ -0,0 +1 @@
legacy-peer-deps=true

13
client/.storybook/main.js Normal file
View file

@ -0,0 +1,13 @@
module.exports = {
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/preset-create-react-app',
],
framework: '@storybook/react',
core: {
builder: '@storybook/builder-webpack5',
},
};

View file

@ -0,0 +1,100 @@
import { useMemo } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { CssBaseline } from '@mui/material';
import { ThemeProvider } from '@mui/material/styles';
import '@storybook/addon-console';
import { initialize, mswDecorator } from 'msw-storybook-addon';
import { cookieDecorator } from 'storybook-addon-cookie';
import { darkTheme, lightTheme } from '../src/themes';
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
options: {
storySort: {
method: 'alphabetical',
},
},
layout: 'fullscreen',
viewport: {
viewports: {
laptop: {
name: 'Laptop',
styles: {
width: '1024px',
height: '576px',
},
type: 'desktop',
},
tablet: {
name: 'Tablet',
styles: {
width: '768px',
height: '1024px',
},
type: 'Tablet',
},
mobile: {
name: 'Mobile',
styles: {
width: '360px',
height: '740px',
},
type: 'mobile',
},
},
},
};
export const globalTypes = {
theme: {
name: 'Theme',
title: 'Theme',
defaultValue: 'light',
toolbar: {
icon: 'paintbrush',
dynamicTitle: true,
items: [
{ value: 'light', title: 'Light Mode', left: '🌞' },
{ value: 'dark', title: 'Dark Mode', left: '🌙' },
],
},
},
};
const THEMES = {
light: lightTheme,
dark: darkTheme,
};
export const withMuiTheme = (Story, context) => {
const { theme: themeKey } = context.globals;
const theme = useMemo(() => THEMES[themeKey] || THEMES['light'], [themeKey]);
return (
<ThemeProvider theme={theme}>
<CssBaseline enableColorScheme />
<Story />
</ThemeProvider>
);
};
export const withBrowserRouter = (Story) => {
return (
<MemoryRouter>
<Story />
</MemoryRouter>
);
};
initialize();
export const decorators = [mswDecorator, cookieDecorator, withBrowserRouter, withMuiTheme];

View file

@ -26,12 +26,24 @@
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
"eject": "react-scripts eject",
"storybook": "start-storybook -p 6006 -s public",
"build-storybook": "build-storybook -s public"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
],
"overrides": [
{
"files": [
"**/*.stories.*"
],
"rules": {
"import/no-anonymous-default-export": "off"
}
}
]
},
"browserslist": {
@ -45,5 +57,27 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@storybook/addon-actions": "^6.5.15",
"@storybook/addon-console": "^1.2.3",
"@storybook/addon-essentials": "^6.5.15",
"@storybook/addon-interactions": "^6.5.15",
"@storybook/addon-links": "^6.5.15",
"@storybook/builder-webpack5": "^6.5.15",
"@storybook/manager-webpack5": "^6.5.15",
"@storybook/node-logger": "^6.5.15",
"@storybook/preset-create-react-app": "^4.1.2",
"@storybook/react": "^6.5.15",
"@storybook/testing-library": "^0.0.13",
"babel-plugin-named-exports-order": "^0.0.2",
"msw": "^0.49.2",
"msw-storybook-addon": "^1.6.3",
"prop-types": "^15.8.1",
"storybook-addon-cookie": "^1.0.6",
"webpack": "^5.75.0"
},
"msw": {
"workerDirectory": "public"
}
}

View file

@ -0,0 +1,303 @@
/* eslint-disable */
/* tslint:disable */
/**
* Mock Service Worker (0.49.2).
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
* - Please do NOT serve this file on production.
*/
const INTEGRITY_CHECKSUM = '3d6b9f06410d179a7f7404d4bf4c3c70'
const activeClientIds = new Set()
self.addEventListener('install', function () {
self.skipWaiting()
})
self.addEventListener('activate', function (event) {
event.waitUntil(self.clients.claim())
})
self.addEventListener('message', async function (event) {
const clientId = event.source.id
if (!clientId || !self.clients) {
return
}
const client = await self.clients.get(clientId)
if (!client) {
return
}
const allClients = await self.clients.matchAll({
type: 'window',
})
switch (event.data) {
case 'KEEPALIVE_REQUEST': {
sendToClient(client, {
type: 'KEEPALIVE_RESPONSE',
})
break
}
case 'INTEGRITY_CHECK_REQUEST': {
sendToClient(client, {
type: 'INTEGRITY_CHECK_RESPONSE',
payload: INTEGRITY_CHECKSUM,
})
break
}
case 'MOCK_ACTIVATE': {
activeClientIds.add(clientId)
sendToClient(client, {
type: 'MOCKING_ENABLED',
payload: true,
})
break
}
case 'MOCK_DEACTIVATE': {
activeClientIds.delete(clientId)
break
}
case 'CLIENT_CLOSED': {
activeClientIds.delete(clientId)
const remainingClients = allClients.filter((client) => {
return client.id !== clientId
})
// Unregister itself when there are no more clients
if (remainingClients.length === 0) {
self.registration.unregister()
}
break
}
}
})
self.addEventListener('fetch', function (event) {
const { request } = event
const accept = request.headers.get('accept') || ''
// Bypass server-sent events.
if (accept.includes('text/event-stream')) {
return
}
// Bypass navigation requests.
if (request.mode === 'navigate') {
return
}
// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
return
}
// Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests
// after it's been deleted (still remains active until the next reload).
if (activeClientIds.size === 0) {
return
}
// Generate unique request ID.
const requestId = Math.random().toString(16).slice(2)
event.respondWith(
handleRequest(event, requestId).catch((error) => {
if (error.name === 'NetworkError') {
console.warn(
'[MSW] Successfully emulated a network error for the "%s %s" request.',
request.method,
request.url,
)
return
}
// At this point, any exception indicates an issue with the original request/response.
console.error(
`\
[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`,
request.method,
request.url,
`${error.name}: ${error.message}`,
)
}),
)
})
async function handleRequest(event, requestId) {
const client = await resolveMainClient(event)
const response = await getResponse(event, client, requestId)
// Send back the response clone for the "response:*" life-cycle events.
// Ensure MSW is active and ready to handle the message, otherwise
// this message will pend indefinitely.
if (client && activeClientIds.has(client.id)) {
;(async function () {
const clonedResponse = response.clone()
sendToClient(client, {
type: 'RESPONSE',
payload: {
requestId,
type: clonedResponse.type,
ok: clonedResponse.ok,
status: clonedResponse.status,
statusText: clonedResponse.statusText,
body:
clonedResponse.body === null ? null : await clonedResponse.text(),
headers: Object.fromEntries(clonedResponse.headers.entries()),
redirected: clonedResponse.redirected,
},
})
})()
}
return response
}
// Resolve the main client for the given event.
// Client that issues a request doesn't necessarily equal the client
// that registered the worker. It's with the latter the worker should
// communicate with during the response resolving phase.
async function resolveMainClient(event) {
const client = await self.clients.get(event.clientId)
if (client?.frameType === 'top-level') {
return client
}
const allClients = await self.clients.matchAll({
type: 'window',
})
return allClients
.filter((client) => {
// Get only those clients that are currently visible.
return client.visibilityState === 'visible'
})
.find((client) => {
// Find the client ID that's recorded in the
// set of clients that have registered the worker.
return activeClientIds.has(client.id)
})
}
async function getResponse(event, client, requestId) {
const { request } = event
const clonedRequest = request.clone()
function passthrough() {
// Clone the request because it might've been already used
// (i.e. its body has been read and sent to the client).
const headers = Object.fromEntries(clonedRequest.headers.entries())
// Remove MSW-specific request headers so the bypassed requests
// comply with the server's CORS preflight check.
// Operate with the headers as an object because request "Headers"
// are immutable.
delete headers['x-msw-bypass']
return fetch(clonedRequest, { headers })
}
// Bypass mocking when the client is not active.
if (!client) {
return passthrough()
}
// Bypass initial page load requests (i.e. static assets).
// The absence of the immediate/parent client in the map of the active clients
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
// and is not ready to handle requests.
if (!activeClientIds.has(client.id)) {
return passthrough()
}
// Bypass requests with the explicit bypass header.
// Such requests can be issued by "ctx.fetch()".
if (request.headers.get('x-msw-bypass') === 'true') {
return passthrough()
}
// Notify the client that a request has been intercepted.
const clientMessage = await sendToClient(client, {
type: 'REQUEST',
payload: {
id: requestId,
url: request.url,
method: request.method,
headers: Object.fromEntries(request.headers.entries()),
cache: request.cache,
mode: request.mode,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body: await request.text(),
bodyUsed: request.bodyUsed,
keepalive: request.keepalive,
},
})
switch (clientMessage.type) {
case 'MOCK_RESPONSE': {
return respondWithMock(clientMessage.data)
}
case 'MOCK_NOT_FOUND': {
return passthrough()
}
case 'NETWORK_ERROR': {
const { name, message } = clientMessage.data
const networkError = new Error(message)
networkError.name = name
// Rejecting a "respondWith" promise emulates a network error.
throw networkError
}
}
return passthrough()
}
function sendToClient(client, message) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel()
channel.port1.onmessage = (event) => {
if (event.data && event.data.error) {
return reject(event.data.error)
}
resolve(event.data)
}
client.postMessage(message, [channel.port2])
})
}
function sleep(timeMs) {
return new Promise((resolve) => {
setTimeout(resolve, timeMs)
})
}
async function respondWithMock(response) {
await sleep(response.delay)
return new Response(response.body, response)
}

View file

@ -1,16 +1,14 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Box, Typography } from '@mui/material';
import { Form } from '.';
import { RecordMp3 } from '../controllers';
import { FormSubmit, FormTextField } from './StyledElements';
function EditUserForm({ form, error, handleSubmit, handleChange }) {
const linkStyle = { textDecoration: 'none', color: 'inherit' };
return (
<Form text='Update Account Details'>
<Form text='Update Account Details' error={error}>
<form onSubmit={handleSubmit}>
<FormTextField
required
@ -40,13 +38,6 @@ function EditUserForm({ form, error, handleSubmit, handleChange }) {
onChange={handleChange}
value={form.pronouns}
/>
{error && (
<Box textAlign='right'>
<Typography variant='body2' color='secondary'>
{error}
</Typography>
</Box>
)}
<FormSubmit color='primary' type='submit'>
Update Details
</FormSubmit>
@ -56,7 +47,6 @@ function EditUserForm({ form, error, handleSubmit, handleChange }) {
</Link>
</FormSubmit>
</form>
<RecordMp3 />
</Form>
);
}

View file

@ -3,8 +3,6 @@ import DarkModeIcon from '@mui/icons-material/DarkMode';
import LightModeIcon from '@mui/icons-material/LightMode';
import LoginIcon from '@mui/icons-material/Login';
import LogoutIcon from '@mui/icons-material/Logout';
import MailIcon from '@mui/icons-material/Mail';
import SearchIcon from '@mui/icons-material/Search';
import MenuIcon from '@mui/icons-material/Menu';
import {
AppBar,
@ -25,7 +23,7 @@ import { Link } from 'react-router-dom';
export default function MobileNavbar({ logout, userData, open, setOpen, mode, toggleMode }) {
function ControlButtons() {
return userData.username ? (
return userData?.email ? (
<ListItem>
<ListItemButton color='inherit' component={Link} to='/login' onClick={logout}>
<ListItemIcon>

View file

@ -6,7 +6,7 @@ import { Link } from 'react-router-dom';
export default function Navbar({ logout, userData, mode, toggleMode }) {
function NavbarControls() {
return userData.email ? (
return userData?.email ? (
<>
<Button color='inherit' component={Link} to='/update'>
Account

View file

@ -21,9 +21,12 @@ function OrganizationList({ orgs }) {
<Typography variant='h6'>Organizations</Typography>
<hr />
<Box sx={{ overflowY: 'auto' }}>
{orgs.map((org, index) => (
<OrgUnit org={org} key={index} />
))}
{orgs.length === 0 ? (
<Typography variant='body1'>Nothing to see here</Typography>
) : (
orgs.map((org, index) => <OrgUnit org={org} key={index} />)
)}
{}
</Box>
</Paper>
);

View file

@ -1,5 +1,5 @@
import { Link } from 'react-router-dom';
import { Box, Typography } from '@mui/material';
import { Typography } from '@mui/material';
import Form from './Form';
import { FormSubmit, FormTextField } from './StyledElements';

View file

@ -11,8 +11,9 @@ import RecoverForm from './RecoverForm';
import ResetForm from './ResetForm';
import OrganizationList from './OrganizationList';
import MemberUnit from './MemberUnit';
import Dashboard from './Dashboard';
import OrganizationDashboard from './OrganizationDashboard';
import UpdateOrganizationForm from './UpdateOrganizationForm';
import RegisterForm from './RegisterForm';
export {
Navbar,
@ -28,6 +29,7 @@ export {
ResetForm,
OrganizationList,
MemberUnit,
Dashboard,
OrganizationDashboard,
UpdateOrganizationForm,
RegisterForm,
};

View file

@ -1,14 +1,26 @@
import Helmet from 'react-helmet';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import Cookies from 'js-cookie';
import { EditUserForm } from '../controllers';
import { EditUserForm, RecordMp3 } from '../controllers';
function Login() {
const navigate = useNavigate();
useEffect(() => {
if (!Cookies.get('token')) {
navigate('/login');
}
}, [navigate]);
return (
<>
<Helmet>
<title>Edit User | KnowItAll</title>
<title>Update Account Details | KnowItAll</title>
</Helmet>
<EditUserForm />
<RecordMp3 />
</>
);
}

View file

@ -1,27 +1,22 @@
import { Box, CssBaseline } from '@mui/material';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { ThemeProvider } from '@mui/material/styles';
import { Helmet } from 'react-helmet';
import { Outlet } from 'react-router-dom';
import { useMode } from '../contexts';
import { Navbar } from '../controllers';
import { lightTheme, darkTheme } from '../themes';
const THEMES = {
light: lightTheme,
dark: darkTheme,
};
function Layout() {
const { mode } = useMode();
const theme = createTheme({
palette: { mode },
breakpoints: {
values: {
xs: 0,
sm: 768,
md: 1024,
},
},
});
return (
<ThemeProvider theme={theme}>
<ThemeProvider theme={THEMES[mode]}>
<CssBaseline enableColorScheme />
<Helmet>
<title>KnowItAll</title>

View file

@ -12,7 +12,7 @@ function MainPage() {
if (!Cookies.get('token')) {
navigate('/login');
}
}, []);
}, [navigate]);
return (
<>

View file

@ -1,8 +1,18 @@
import Helmet from 'react-helmet';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import Cookies from 'js-cookie';
import { MemberOrgsDashboard } from '../controllers';
function MemberOrgs() {
const navigate = useNavigate();
useEffect(() => {
if (!Cookies.get('token')) {
navigate('/login');
}
}, [navigate]);
return (
<>
<Helmet>

View file

@ -1,14 +1,26 @@
import Helmet from 'react-helmet';
import { Dashboard } from '../controllers';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import Cookies from 'js-cookie';
import { OrganizationDashboard } from '../controllers';
function OrgDashboard() {
const navigate = useNavigate();
useEffect(() => {
if (!Cookies.get('token')) {
navigate('/login');
}
}, [navigate]);
return (
<>
<Helmet>
<title>Dashboard | KnowItAll</title>
</Helmet>
<Dashboard />
<OrganizationDashboard />
</>
);
}

View file

@ -1,8 +1,20 @@
import Helmet from 'react-helmet';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import Cookies from 'js-cookie';
import { OwnedOrgsDashboard } from '../controllers';
function OwnedOrgs() {
const navigate = useNavigate();
useEffect(() => {
if (!Cookies.get('token')) {
navigate('/login');
}
}, [navigate]);
return (
<>
<Helmet>

View file

@ -1,8 +1,20 @@
import Helmet from 'react-helmet';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import Cookies from 'js-cookie';
import { UpdateOrganizationForm } from '../controllers';
function UpdateOrganization() {
const navigate = useNavigate();
useEffect(() => {
if (!Cookies.get('token')) {
navigate('/login');
}
}, [navigate]);
return (
<>
<Helmet>

View file

@ -1,6 +1,6 @@
import { createContext, useContext, useState } from 'react';
const ErrorContext = createContext();
export const ErrorContext = createContext();
export default function ModeProvider({ children }) {
const [error, setError] = useState('');

View file

@ -1,7 +1,7 @@
import { createContext, useContext, useState, useEffect } from 'react';
import Cookies from 'js-cookie';
const ModeContext = createContext();
export const ModeContext = createContext();
export default function ModeProvider({ children }) {
const [mode, setMode] = useState(Cookies.get('theme') || 'light');

View file

@ -7,7 +7,7 @@ import { remember } from '../services/userServices';
const initialUserData = { loading: true };
const UserContext = createContext();
export const UserContext = createContext();
export default function UserProvider({ children }) {
const [userData, setUserData] = useState(initialUserData);
@ -26,7 +26,7 @@ export default function UserProvider({ children }) {
};
if (userData.loading) loadUserData();
}, [userData.loading]);
}, [userData.loading, navigate]);
return (
<UserContext.Provider

View file

@ -1,6 +1,17 @@
import ModeProvider, { useMode } from './ModeContext';
import UserProvider, { useUser } from './UserContext';
import ErrorProvider, { useError } from './ErrorContext';
import ModeProvider, { useMode, ModeContext } from './ModeContext';
import UserProvider, { useUser, UserContext } from './UserContext';
import ErrorProvider, { useError, ErrorContext } from './ErrorContext';
import ContextProvider from './ContextProvider';
export { ModeProvider, useMode, UserProvider, useUser, ErrorProvider, useError, ContextProvider };
export {
ModeProvider,
useMode,
UserProvider,
useUser,
ErrorProvider,
useError,
ContextProvider,
ErrorContext,
ModeContext,
UserContext,
};

View file

@ -29,12 +29,14 @@ function EditUserFormController() {
async function handleSubmit(e) {
e.preventDefault();
const { error } = await updateUserDetails(form);
if (error) {
setError(() => error);
} else {
try {
await updateUserDetails(form);
navigate('/');
} catch (error) {
const message = error.response.data;
setError(() => message.error);
}
}

View file

@ -36,12 +36,13 @@ function LoginFormController({ setToken }) {
async function handleSubmit(e) {
e.preventDefault();
const { error } = await login(form);
if (error) {
setError(() => error);
} else {
try {
await login(form);
navigate('/');
} catch (error) {
const message = error.response.data;
setError(() => message.error);
}
}

View file

@ -35,29 +35,52 @@ function MainPageController() {
}
async function retrieveOwnedOrgs() {
const { orgs } = await getOwnedOrgs().catch((e) => console.log(e));
try {
const { orgs } = await getOwnedOrgs().catch((e) => console.error(e));
setOwnedOrgs(() => orgs);
} catch (error) {
const message = error.response.data;
setOwnedOrgs(() => orgs);
console.error(message.error);
}
}
async function retrieveMemberOrgs() {
const { orgs } = await getMemberOrgs().catch((e) => console.log(e));
try {
const { orgs } = await getMemberOrgs().catch((e) => console.error(e));
setOrgs(() => orgs);
} catch (error) {
const message = error.response.data;
setOrgs(() => orgs);
console.error(message.error);
}
}
async function makeOrg(e) {
e.preventDefault();
await createOrg({ orgName: form.name })
.then(retrieveOwnedOrgs)
.catch((e) => console.log(e));
try {
await createOrg({ orgName: form.name })
.then(retrieveOwnedOrgs)
.catch((e) => console.log(e));
} catch (error) {
const message = error.response.data;
console.error(message.error);
}
}
async function joinOrg(e) {
e.preventDefault();
await addMember.then(retrieveMemberOrgs).catch((e) => console.log(e));
try {
await addMember(form.id);
await retrieveMemberOrgs();
} catch (error) {
const message = error.response.data;
console.error(message.error);
}
}
return (

View file

@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react';
import Cookies from 'js-cookie';
import { useEffect, useState } from 'react';
import { OrganizationList } from '../components';
import { getMemberOrgs } from '../services/userServices';
@ -6,13 +7,21 @@ import { getMemberOrgs } from '../services/userServices';
function MemberOrgsDashboard() {
const [orgs, setOrgs] = useState([]);
async function getOrgs() {
getMemberOrgs().then((res) => {
setOrgs(() => res.orgs);
});
function getOrgs() {
if (Cookies.get('token')) {
getMemberOrgs()
.then((res) => {
setOrgs(() => res.orgs);
})
.catch((error) => {
const message = error.response.data;
console.error(message.error);
});
}
}
useEffect(getOrgs);
useEffect(getOrgs, []);
return <OrganizationList orgs={orgs} />;
}

View file

@ -17,7 +17,6 @@ export default function NavbarController() {
const toggleOpen = () => setOpen((initial) => !initial);
const toggleMode = () => {
console.log('Changed mode');
setMode(() => (mode === 'light' ? 'dark' : 'light'));
};
@ -25,7 +24,14 @@ export default function NavbarController() {
const md = useMediaQuery((theme) => theme.breakpoints.only('md'));
const logoutUser = async () => {
await logout();
try {
await logout();
} catch (error) {
const message = error.response.data;
console.error(message.error);
}
Cookies.remove('token');
setUserData({});
navigate('/login');

View file

@ -1,11 +1,11 @@
import React, { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { Dashboard } from '../components';
import { OrganizationDashboard } from '../components';
import { getAudio } from '../services/userServices';
import { getOrg, deleteOrg, removeMember } from '../services/orgServices';
function DashboardController() {
function OrganizationDashboardController() {
const { orgID } = useParams();
const navigate = useNavigate();
@ -23,7 +23,7 @@ function DashboardController() {
});
const [rows, setRows] = useState([]);
const [status, setStatus] = useState(true);
const [selection, setSelection] = useState(null);
const [selection, setSelection] = useState([]);
const [open, setOpen] = useState(false);
useEffect(() => {
@ -38,7 +38,7 @@ function DashboardController() {
setStatus(() => res.owner);
})
.catch((e) => {
console.log(e);
console.error(e.response.data.error);
navigate('/');
});
}
@ -51,7 +51,12 @@ function DashboardController() {
}
async function handleDelete() {
await deleteOrg(orgID).then(navigate('/'));
await deleteOrg(orgID)
.then(navigate('/'))
.catch((e) => {
console.error(e.response.data.error);
navigate('/');
});
}
function copyID() {
@ -67,26 +72,40 @@ function DashboardController() {
async function removeSelected() {
const doomedUserIDs = selection.map((row) => row.id);
await removeMember(orgID, doomedUserIDs);
try {
await removeMember(orgID, doomedUserIDs);
} catch (error) {
const message = error.response.data;
console.error(message.error);
}
}
async function leaveOrg() {
await removeMember(orgID, {}).then(navigate('/'));
await removeMember(orgID, {})
.then(navigate('/'))
.catch((e) => {
console.error(e.response.data.error);
});
}
async function play(id) {
await getAudio(id).then((res) => {
const uploadedFile = new File([res], 'userAudio.mp3', {
type: res.type,
lastModified: Date.now(),
await getAudio(id)
.then((res) => {
const uploadedFile = new File([res], 'userAudio.mp3', {
type: res.type,
lastModified: Date.now(),
});
const audioFile = new Audio(URL.createObjectURL(uploadedFile));
audioFile.play();
})
.catch((e) => {
console.error(e.response.data.error);
});
const audioFile = new Audio(URL.createObjectURL(uploadedFile));
audioFile.play();
});
}
return (
<Dashboard
<OrganizationDashboard
org={org}
rows={rows}
open={open}
@ -103,4 +122,4 @@ function DashboardController() {
);
}
export default DashboardController;
export default OrganizationDashboardController;

View file

@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import Cookies from 'js-cookie';
import { OrganizationList } from '../components';
import { getOwnedOrgs } from '../services/userServices';
@ -6,13 +7,19 @@ import { getOwnedOrgs } from '../services/userServices';
function OwnedOrgsDashboard() {
const [orgs, setOrgs] = useState([]);
async function getOrgs() {
getOwnedOrgs().then((res) => {
setOrgs(() => res.orgs);
});
function getOrgs() {
if (Cookies.get('token')) {
getOwnedOrgs()
.then((res) => {
setOrgs(() => res.orgs);
})
.catch((e) => {
console.error(e.response.data.error);
});
}
}
useEffect(getOrgs);
useEffect(getOrgs, []);
return <OrganizationList orgs={orgs} />;
}

View file

@ -1,5 +1,4 @@
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import Cookies from 'js-cookie';
import MicRecorder from 'mic-recorder-to-mp3';
@ -21,19 +20,31 @@ function RecordMp3Controller() {
useEffect(() => {
if (!userData.loading) {
getAudio(userData.id).then((data) => {
setUploadedFile(() => {
// Retrieve audio file of user if exists
return new File([data], 'userAudio.mp3', {
type: data.type,
lastModified: Date.now(),
getAudio(userData.id)
.then((data) => {
if (!data) {
return;
}
setUploadedFile(() => {
// Retrieve audio file of user if exists
return new File([data], 'userAudio.mp3', {
type: data.type,
lastModified: Date.now(),
});
});
})
.catch((e) => {
console.error(e.response.data.error);
});
});
}
}, [userData]);
function togglePlay() {
if (!uploadedFile) {
return;
}
if (audioRecording && !audioRecording.ended) {
// Reset audio and reset if track is playing
audioRecording.pause();
@ -85,7 +96,9 @@ function RecordMp3Controller() {
formData.append('audioFile', uploadedFile, 'userAudio.mp3');
formData.append('token', Cookies.get('token'));
await setAudio(formData);
setAudio(formData).catch((e) => {
console.error(e.response.data.error);
});
}
return <RecordMp3 {...{ togglePlay, record, uploadRecord, handleSubmit }} />;

View file

@ -20,7 +20,14 @@ function RecoverFormController() {
async function handleSubmit(e) {
e.preventDefault();
await requestReset(form);
try {
await requestReset(form);
} catch (error) {
const message = error.response.data;
setError(message.error);
}
}
return (

View file

@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import RegisterForm from '../components/RegisterForm';
import { RegisterForm } from '../components';
import { useError } from '../contexts';
import { register } from '../services/userServices';
@ -32,12 +32,14 @@ function RegisterFormController() {
e.preventDefault();
const { confirmPassword, ...filteredForm } = form;
const { error } = await register({ ...filteredForm, orgID: params.get('orgID') });
if (error) {
setError(() => error);
} else {
try {
await register({ ...filteredForm, orgID: params.get('orgID') });
navigate('/login');
} catch (error) {
const message = error.response.data;
setError(message.error);
}
}

View file

@ -27,12 +27,13 @@ function ResetFormController() {
async function handleSubmit(e) {
e.preventDefault();
const error = await resetPassword(id, { password: form.password });
if (error) {
setError(() => error);
} else {
try {
await resetPassword(id, { password: form.password });
navigate('/login');
} catch (error) {
const message = error.response.data;
setError(message.error);
}
}

View file

@ -19,9 +19,13 @@ function UpdateOrganizationFormController() {
async function handleSubmit(e) {
e.preventDefault();
await updateOrgDetails(orgID, { orgName: name }).then(() => {
navigate(`/org/${orgID}`);
});
await updateOrgDetails(orgID, { orgName: name })
.then(() => {
navigate(`/org/${orgID}`);
})
.catch((e) => {
console.error(e.response.data.error);
});
}
return (

View file

@ -8,7 +8,7 @@ import RecoverForm from './RecoverFormController';
import ResetForm from './ResetFormController';
import OwnedOrgsDashboard from './OwnedOrgsDashboard';
import MemberOrgsDashboard from './MemberOrgsDashboard';
import Dashboard from './DashboardController';
import OrganizationDashboard from './OrganizationDashboardController';
import UpdateOrganizationForm from './UpdateOrganizationFormController';
export {
@ -22,6 +22,6 @@ export {
ResetForm,
OwnedOrgsDashboard,
MemberOrgsDashboard,
Dashboard,
OrganizationDashboard,
UpdateOrganizationForm,
};

View file

@ -6,74 +6,43 @@ async function addMember(id) {
const instance = createRequest(`/org`);
instance.defaults.headers.common['Authorization'] = `bearer ${Cookies.get('token')}`;
try {
await instance.post(`/${id}/add`);
} catch (error) {
console.log(error.response.data);
return error;
}
await instance.post(`/${id}/add`);
}
async function createOrg(data) {
const instance = createRequest('/org');
instance.defaults.headers.common['Authorization'] = `bearer ${Cookies.get('token')}`;
try {
await instance.post('/', data);
} catch (error) {
console.log(error.response.data);
return error;
}
await instance.post('/', data);
}
async function deleteOrg(id) {
const instance = createRequest('/org');
instance.defaults.headers.common['Authorization'] = `bearer ${Cookies.get('token')}`;
try {
await instance.delete(`/${id}`);
} catch (error) {
console.log(error.response.data);
return error;
}
await instance.delete(`/${id}`);
}
async function getOrg(id) {
const instance = createRequest('/org');
instance.defaults.headers.common['Authorization'] = `bearer ${Cookies.get('token')}`;
try {
const response = await instance.get(`/${id}`);
console.log(response.data);
return response.data;
} catch (error) {
console.log(error.response.data);
return error;
}
const response = await instance.get(`/${id}`);
return response.data;
}
async function removeMember(id, doomedUsers) {
const instance = createRequest('/org');
instance.defaults.headers.common['Authorization'] = `bearer ${Cookies.get('token')}`;
try {
await instance.post(`/${id}/remove`, { doomedUsers });
} catch (error) {
console.log(error.response.data);
return error;
}
await instance.post(`/${id}/remove`, { doomedUsers });
}
async function updateOrgDetails(id, data) {
const instance = createRequest('/org');
instance.defaults.headers.common['Authorization'] = `bearer ${Cookies.get('token')}`;
try {
await instance.patch(`/${id}`, { data });
} catch (error) {
console.log(error.response.data);
return error;
}
await instance.patch(`/${id}`, { data });
}
export { addMember, createOrg, deleteOrg, getOrg, removeMember, updateOrgDetails };

View file

@ -10,43 +10,28 @@ async function login({ email, password, remember }) {
const instance = createRequest('/auth');
instance.defaults.headers.common['Authorization'] = `basic ${encoded}`;
try {
const { data } = await instance.post('/login', { remember });
const { data } = await instance.post('/login', { remember });
if (remember) {
Cookies.set('token', data.token, { expires: 365 });
} else {
Cookies.set('token', data.token, { expires: 1 });
}
return data;
} catch (error) {
console.log(error.response.data);
return error;
if (remember) {
Cookies.set('token', data.token, { expires: 365 });
} else {
Cookies.set('token', data.token, { expires: 1 });
}
return data;
}
async function register(data) {
const instance = createRequest('/auth');
try {
await instance.post('/register', data);
} catch (error) {
console.log(error.response.data);
return error;
}
await instance.post('/register', data);
}
async function logout() {
const instance = createRequest('/auth');
instance.defaults.headers.common['Authorization'] = `bearer ${Cookies.get('token')}`;
try {
await instance.post('/logout');
} catch (error) {
console.log(error.response.data);
return error;
}
await instance.post('/logout');
}
async function remember() {
@ -54,50 +39,29 @@ async function remember() {
instance.defaults.headers.common['Authorization'] = `bearer ${Cookies.get('token')}`;
if (Cookies.get('token')) {
try {
const { data } = await instance.post('/remember');
return data;
} catch (error) {
console.log(error.response.data);
return error.response.data;
}
const { data } = await instance.post('/remember');
return data;
}
}
async function getUser(userID) {
const instance = createRequest('/auth');
try {
const { data } = await instance.get(`/${userID}`);
const { data } = await instance.get(`/${userID}`);
return data.user;
} catch (error) {
console.log(error.response.data);
return error.response.data;
}
return data.user;
}
async function requestReset(data) {
const instance = createRequest('/auth');
try {
await instance.post(`/reset`, data);
} catch (error) {
console.log(error.response.data);
return error.response.data;
}
await instance.post(`/reset`, data);
}
async function resetPassword(id, data) {
const instance = createRequest('/auth');
try {
await instance.patch(`/reset/${id}`, data);
} catch (error) {
console.log(error.response.data);
return error.response.data;
}
await instance.patch(`/reset/${id}`, data);
}
async function setAudio(data) {
@ -105,12 +69,7 @@ async function setAudio(data) {
instance.defaults.headers.common['Content-Type'] = 'multipart/form-data';
instance.defaults.headers.common['Authorization'] = `bearer ${Cookies.get('token')}`;
try {
await instance.post('/audio', data);
} catch (error) {
console.log(error.response.data);
return error.response.data;
}
await instance.post('/audio', data);
}
async function getAudio(userID) {
@ -128,40 +87,25 @@ async function updateUserDetails(data) {
const instance = createRequest('/auth');
instance.defaults.headers.common['Authorization'] = `bearer ${Cookies.get('token')}`;
try {
await instance.post(`/update`, data);
} catch (error) {
console.log(error.response.data);
return error.response.data;
}
await instance.post(`/update`, data);
}
async function getMemberOrgs() {
const instance = createRequest('/auth');
instance.defaults.headers.common['Authorization'] = `bearer ${Cookies.get('token')}`;
try {
const { data } = await instance.get(`/orgs/member`);
const { data } = await instance.get(`/orgs/member`);
return data;
} catch (error) {
console.log(error.response.data);
return error.response.data;
}
return data;
}
async function getOwnedOrgs() {
const instance = createRequest('/auth');
instance.defaults.headers.common['Authorization'] = `bearer ${Cookies.get('token')}`;
try {
const { data } = await instance.get(`/orgs`);
const { data } = await instance.get(`/orgs`);
return data;
} catch (error) {
console.log(error.response.data);
return error.response.data;
}
return data;
}
export {

View file

@ -0,0 +1,55 @@
import { rest } from 'msw';
import { ErrorProvider, UserContext } from '../contexts';
import { EditUserForm } from '../controllers';
export default {
title: 'Forms/EditUser',
component: EditUserForm,
};
const Template = (args) => (
<UserContext.Provider
value={{
userData: args,
}}
>
<ErrorProvider>
<EditUserForm />
</ErrorProvider>
</UserContext.Provider>
);
export const WithIntialData = Template.bind({});
WithIntialData.args = {
firstName: 'Test',
lastName: 'User',
email: 'test.user@test.com',
pronouns: 'they/them',
};
WithIntialData.parameters = {
msw: {
handlers: [
rest.post(`${process.env.REACT_APP_API_ROOT}/auth/update`, (req, res, ctx) => {
return res(ctx.status(200));
}),
],
},
};
export const WithoutInitialData = Template.bind({});
WithoutInitialData.args = {
firstName: '',
lastName: '',
email: '',
pronouns: '',
};
WithoutInitialData.parameters = {
msw: {
handlers: [
rest.post(`${process.env.REACT_APP_API_ROOT}/auth/update`, (req, res, ctx) => {
return res(ctx.status(200));
}),
],
},
};

View file

@ -0,0 +1,14 @@
import { Form } from '../components';
export default {
title: 'Layout/Form',
component: Form,
args: {
text: 'Test Form',
error: '',
},
};
const Template = (args) => <Form {...args} />;
export const Primary = Template.bind({});

View file

@ -0,0 +1,26 @@
import { rest } from 'msw';
import { LoginForm } from '../controllers';
import { ErrorProvider } from '../contexts';
export default {
title: 'Forms/Login',
component: LoginForm,
};
const Template = (args) => (
<ErrorProvider>
<LoginForm {...args} />
</ErrorProvider>
);
export const Primary = Template.bind({});
Primary.parameters = {
msw: {
handlers: [
rest.post(`${process.env.REACT_APP_API_ROOT}/auth/login`, (req, res, ctx) => {
return res(ctx.status(200));
}),
],
},
};

View file

@ -0,0 +1,64 @@
import { rest } from 'msw';
import { MainPageDisplay } from '../controllers';
import { ErrorProvider } from '../contexts';
export default {
title: 'Pages/Main Page',
component: MainPageDisplay,
parameters: {
msw: {
handlers: [
rest.get(`${process.env.REACT_APP_API_ROOT}/auth/orgs`, (req, res, ctx) => {
return res(
ctx.json({
orgs: [
{
id: 'fcfce355-7877-4a7f-a762-33ead250daa2',
name: 'Test Org Owned',
memberCount: 42,
createdAt: 1672202132563,
},
],
})
);
}),
rest.get(`${process.env.REACT_APP_API_ROOT}/auth/orgs/member`, (req, res, ctx) => {
return res(
ctx.json({
orgs: [
{
id: 'fcfce355-7877-4a7f-a762-33ead250daa2',
name: 'Test Org Member',
memberCount: 42,
createdAt: 1672202132563,
},
],
})
);
}),
rest.post(`${process.env.REACT_APP_API_ROOT}/org/*/add`, (req, res, ctx) => {
return res(ctx.status(200));
}),
rest.post(`${process.env.REACT_APP_API_ROOT}/org`, (req, res, ctx) => {
return res(ctx.status(200));
}),
],
},
},
};
const Template = (args) => (
<ErrorProvider>
<MainPageDisplay {...args} />
</ErrorProvider>
);
export const WithOrganizations = Template.bind({});
WithOrganizations.parameters = {
cookie: {
token: 'testtoken',
},
};
export const WithoutOrganizations = Template.bind({});

View file

@ -0,0 +1,44 @@
import { rest } from 'msw';
import { MemberOrgsDashboard } from '../controllers';
import { ErrorProvider } from '../contexts';
export default {
title: 'Pages/Member Dashboard',
component: MemberOrgsDashboard,
parameters: {
msw: {
handlers: [
rest.get(`${process.env.REACT_APP_API_ROOT}/auth/orgs/member`, (req, res, ctx) => {
return res(
ctx.json({
orgs: [
{
id: 'fcfce355-7877-4a7f-a762-33ead250daa2',
name: 'Test Org Member',
memberCount: 42,
createdAt: 1672202132563,
},
],
})
);
}),
],
},
},
};
const Template = (args) => (
<ErrorProvider>
<MemberOrgsDashboard {...args} />
</ErrorProvider>
);
export const WithOrganizations = Template.bind({});
WithOrganizations.parameters = {
cookie: {
token: 'testToken',
},
};
export const WithoutOrganizations = Template.bind({});

View file

@ -0,0 +1,24 @@
import { MemberUnit } from '../components';
export default {
title: 'Components/MemberUnit',
component: MemberUnit,
argTypes: {
onClick: { action: 'playAudio' },
},
};
const Template = (args) => {
const { onClick, ...member } = args;
return <MemberUnit member={member} onClick={onClick} />;
};
export const Primary = Template.bind({});
Primary.args = {
id: 'fcfce355-7877-4a7f-a762-33ead250daa2',
firstName: 'Test',
lastName: 'User',
nickname: 'TU',
pronouns: 'they/them',
email: 'test.user@know-it-all.com',
};

View file

@ -0,0 +1,38 @@
import { rest } from 'msw';
import { ModeProvider, UserContext } from '../contexts';
import { Navbar } from '../controllers';
export default {
title: 'Layout/Navbar',
component: Navbar,
parameters: {
msw: {
handlers: [
rest.post(`${process.env.REACT_APP_API_ROOT}/auth/logout`, (req, res, ctx) => {
return res(ctx.json({}));
}),
],
},
},
};
const Template = (args) => (
<UserContext.Provider
value={{
setUserData: () => {},
userData: args,
}}
>
<ModeProvider>
<Navbar />
</ModeProvider>
</UserContext.Provider>
);
export const LoggedIn = Template.bind({});
LoggedIn.args = {
email: 'test.user@test.com',
};
export const LoggedOut = Template.bind({});

View file

@ -0,0 +1,16 @@
import { OrgUnit } from '../components';
export default {
title: 'Components/OrgUnit',
component: OrgUnit,
};
const Template = (args) => <OrgUnit org={args} />;
export const Primary = Template.bind({});
Primary.args = {
id: 'fcfce355-7877-4a7f-a762-33ead250daa2',
name: 'Test Org',
memberCount: 42,
createdAt: 1672202132563,
};

View file

@ -0,0 +1,84 @@
import { rest } from 'msw';
import { OrganizationDashboard } from '../controllers';
import { ErrorProvider } from '../contexts';
export default {
title: 'Pages/Organization Dashboard',
component: OrganizationDashboard,
};
const Template = (args) => (
<ErrorProvider>
<OrganizationDashboard {...args} />
</ErrorProvider>
);
export const Owner = Template.bind({});
Owner.parameters = {
cookie: {
token: 'testToken',
},
msw: {
handlers: [
rest.get(`${process.env.REACT_APP_API_ROOT}/org/*`, (req, res, ctx) => {
return res(
ctx.json({
org: {
id: 'testID',
name: 'Test Org',
owner: {
firstName: 'Test',
lastName: 'User',
email: 'test.user@test.com',
},
member: [],
createdAt: 1672202132563,
},
memberCount: 0,
owner: true,
})
);
}),
rest.delete(`${process.env.REACT_APP_API_ROOT}/org/*`, (req, res, ctx) => {
return res(ctx.status(200));
}),
rest.post(`${process.env.REACT_APP_API_ROOT}/org/*/remove`, (req, res, ctx) => {
return res(ctx.status(200));
}),
],
},
};
export const NotOwner = Template.bind({});
NotOwner.parameters = {
cookie: {
token: 'testToken',
},
msw: {
handlers: [
rest.get(`${process.env.REACT_APP_API_ROOT}/org/*`, (req, res, ctx) => {
return res(
ctx.json({
org: {
id: 'testID',
name: 'Test Org',
owner: {
firstName: 'Test',
lastName: 'User',
email: 'test.user@test.com',
},
member: [],
createdAt: 1672202132563,
},
memberCount: 0,
owner: false,
})
);
}),
rest.post(`${process.env.REACT_APP_API_ROOT}/org/*/remove`, (req, res, ctx) => {
return res(ctx.status(200));
}),
],
},
};

View file

@ -0,0 +1,44 @@
import { rest } from 'msw';
import { OwnedOrgsDashboard } from '../controllers';
import { ErrorProvider } from '../contexts';
export default {
title: 'Pages/Owned Dashboard',
component: OwnedOrgsDashboard,
parameters: {
msw: {
handlers: [
rest.get(`${process.env.REACT_APP_API_ROOT}/auth/orgs`, (req, res, ctx) => {
return res(
ctx.json({
orgs: [
{
id: 'fcfce355-7877-4a7f-a762-33ead250daa2',
name: 'Test Org Member',
memberCount: 42,
createdAt: 1672202132563,
},
],
})
);
}),
],
},
},
};
const Template = (args) => (
<ErrorProvider>
<OwnedOrgsDashboard {...args} />
</ErrorProvider>
);
export const WithOrganizations = Template.bind({});
WithOrganizations.parameters = {
cookie: {
token: 'testToken',
},
};
export const WithoutOrganizations = Template.bind({});

View file

@ -0,0 +1,40 @@
import { rest } from 'msw';
import { RecordMp3 } from '../controllers';
import { UserContext } from '../contexts';
export default {
title: 'Forms/RecordMp3',
component: RecordMp3,
};
const Template = (args) => (
<UserContext.Provider
value={{
setUserData: () => {},
userData: args,
}}
>
<RecordMp3 />
</UserContext.Provider>
);
export const Primary = Template.bind({});
Primary.args = {
id: 'test-id',
};
Primary.parameters = {
msw: {
handlers: [
rest.get(
`${process.env.REACT_APP_DOMAIN_ROOT}/public/audio/test-id.mp3`,
(req, res, ctx) => {
return res(ctx.status(404));
}
),
rest.post(`${process.env.REACT_APP_API_ROOT}/auth/audio`, (req, res, ctx) => {
return res(ctx.status(200));
}),
],
},
};

View file

@ -0,0 +1,26 @@
import { rest } from 'msw';
import { RecoverForm } from '../controllers';
import { ErrorProvider } from '../contexts';
export default {
title: 'Forms/Recover Password',
component: RecoverForm,
};
const Template = (args) => (
<ErrorProvider>
<RecoverForm {...args} />
</ErrorProvider>
);
export const Primary = Template.bind({});
Primary.parameters = {
msw: {
handlers: [
rest.post(`${process.env.REACT_APP_API_ROOT}/auth/reset/undefined`, (req, res, ctx) => {
return res(ctx.status(200));
}),
],
},
};

View file

@ -0,0 +1,26 @@
import { rest } from 'msw';
import { RegisterForm } from '../controllers';
import { ErrorProvider } from '../contexts';
export default {
title: 'Forms/RegisterForm',
component: RegisterForm,
};
const Template = (args) => (
<ErrorProvider>
<RegisterForm {...args} />
</ErrorProvider>
);
export const Primary = Template.bind({});
Primary.parameters = {
msw: {
handlers: [
rest.post(`${process.env.REACT_APP_API_ROOT}/auth/register`, (req, res, ctx) => {
return res(ctx.status(200));
}),
],
},
};

View file

@ -0,0 +1,29 @@
import { rest } from 'msw';
import { ResetForm } from '../controllers';
import { ErrorProvider } from '../contexts';
export default {
title: 'Forms/Reset Password',
component: ResetForm,
};
const Template = (args) => (
<ErrorProvider>
<ResetForm {...args} />
</ErrorProvider>
);
export const Primary = Template.bind({});
Primary.parameters = {
msw: {
handlers: [
rest.patch(
`${process.env.REACT_APP_API_ROOT}/auth/reset/undefined`,
(req, res, ctx) => {
return res(ctx.status(200));
}
),
],
},
};

View file

@ -0,0 +1,31 @@
import { Stack, Box } from '@mui/material';
import { StackItem } from '../components';
import { ErrorProvider } from '../contexts';
export default {
title: 'Components/Stack Item',
component: StackItem,
args: {
text: 'Default Text',
},
};
export const Single = (args) => (
<ErrorProvider>
<Stack spacing={2} sx={{ padding: 2 }}>
<StackItem {...args} />
</Stack>
</ErrorProvider>
);
export const Multiple = (args) => (
<ErrorProvider>
<Stack spacing={2} sx={{ padding: 2 }}>
<StackItem {...args} />
<StackItem {...args} />
<StackItem {...args} />
<StackItem {...args} />
</Stack>
</ErrorProvider>
);

View file

@ -0,0 +1,26 @@
import { rest } from 'msw';
import { UpdateOrganizationForm } from '../controllers';
import { ErrorProvider } from '../contexts';
export default {
title: 'Forms/Update Organization',
component: UpdateOrganizationForm,
};
const Template = (args) => (
<ErrorProvider>
<UpdateOrganizationForm {...args} />
</ErrorProvider>
);
export const Primary = Template.bind({});
Primary.parameters = {
msw: {
handlers: [
rest.patch(`${process.env.REACT_APP_API_ROOT}/org/undefined`, (req, res, ctx) => {
return res(ctx.status(200));
}),
],
},
};

16
client/src/themes/dark.js Normal file
View file

@ -0,0 +1,16 @@
import { createTheme } from '@mui/material/styles';
const darkTheme = createTheme({
palette: {
mode: 'dark',
},
breakpoints: {
values: {
xs: 0,
sm: 768,
md: 1024,
},
},
});
export { darkTheme };

View file

@ -0,0 +1,4 @@
import { darkTheme } from './dark';
import { lightTheme } from './light';
export { darkTheme, lightTheme };

View file

@ -0,0 +1,16 @@
import { createTheme } from '@mui/material/styles';
const lightTheme = createTheme({
palette: {
mode: 'light',
},
breakpoints: {
values: {
xs: 0,
sm: 768,
md: 1024,
},
},
});
export { lightTheme };

31636
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -5,15 +5,20 @@
"main": "index.js",
"scripts": {
"start": "concurrently \"npm run start -w client\" \"npm run start -w server\"",
"start:server": "npm start server",
"start:client": "npm start -w client",
"start:server": "npm start -w server",
"build": "npm run build -w client",
"build:package": "tar --exclude='.env' -czf dist.tar.gz client node_modules server LICENSE package-lock.json package.json",
"build:package": "tar -czf dist.tar.gz client node_modules server LICENSE package-lock.json package.json",
"dist": "npm run build:package",
"test": "npm run test -w server",
"migrate": "npm run migrate -w server",
"migrate:undo": "npm run migrate:undo -w server",
"predev": "npm run build",
"predist": "npm ci && npm run build"
"predist": "npm ci && npm run build",
"eslint": "npx eslint",
"eslint:fix": "npx eslint --fix",
"eslint:all": "npx eslint '**'",
"eslint:all:fix": "npx eslint '**' --fix",
"storybook": "npm run storybook -w client"
},
"author": "C4 Patino",
"license": "ISC",
@ -29,6 +34,13 @@
},
"devDependencies": {
"concurrently": "^7.6.0",
"eslint": "^8.30.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jest": "^27.1.7",
"eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-react": "^7.31.11",
"eslint-plugin-react-hooks": "^4.6.0",
"sequelize-cli": "^6.4.1"
}
}

View file

@ -1,32 +1,30 @@
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var bodyParser = require('body-parser');
var cors = require('cors');
const createError = require('http-errors');
const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const morgan = require('morgan');
const bodyParser = require('body-parser');
const cors = require('cors');
var authRouter = require('./routes/authRouter');
var orgRouter = require('./routes/orgRouter');
const authRouter = require('./routes/authRouter');
const orgRouter = require('./routes/orgRouter');
var app = express();
const app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
if (process.env.NODE_ENV != 'test') {
app.use(logger('dev'));
if (process.env.NODE_ENV !== 'test') {
app.use(morgan('dev'));
}
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(cors());
app.use(
bodyParser.urlencoded({
extended: true,
})
);
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use('/public', express.static(path.join(__dirname, 'public')));
@ -40,7 +38,7 @@ app.get('*', (req, res) => {
});
// catch 404 and forward to error handler
app.use(function (req, res, next) {
app.use((req, res, next) => {
next(createError(404));
});

View file

@ -1,56 +1,17 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
var debug = require('debug')('server:server');
var http = require('http');
const http = require('http');
require('dotenv').config();
var app = require('../app');
var { sequelize } = require('../db/models/index');
const app = require('../app');
const logger = require('../utils/logger');
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app);
async function connectDatabase() {
try {
await sequelize.authenticate();
console.log(
`[INFO]: Database connection to '${process.env.NODE_ENV}' has been established successfully.`
);
} catch (error) {
console.log('[ERROR]: Unable to connect to the database;', error);
}
}
connectDatabase().then(() => {
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
});
/**
* Normalize a port into a number, string, or false.
*/
const { sequelize } = require('../db/models/index');
function normalizePort(val) {
var port = parseInt(val, 10);
const port = parseInt(val, 10);
if (isNaN(port)) {
if (Number.isNaN(port)) {
// named pipe
return val;
}
@ -63,6 +24,19 @@ function normalizePort(val) {
return false;
}
/**
* Get port from environment and store in Express.
*/
const port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
const server = http.createServer(app);
/**
* Event listener for HTTP server "error" event.
*/
@ -72,16 +46,16 @@ function onError(error) {
throw error;
}
var bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port;
const bind = typeof port === 'string' ? `Pipe ${port}` : `Port ${port}`;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
logger.error(`${bind} requires elevated privileges`);
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
logger.error(`${bind} is already in use`);
process.exit(1);
break;
default:
@ -94,7 +68,28 @@ function onError(error) {
*/
async function onListening() {
var addr = server.address();
var bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port;
debug('Listening on ' + bind);
const addr = server.address();
const bind = typeof addr === 'string' ? `pipe ${addr}` : `port ${addr.port}`;
logger.info(`Listening on ${bind}`);
}
async function connectDatabase() {
try {
await sequelize.authenticate();
logger.info(
`Database connection to '${process.env.NODE_ENV}' has been established successfully.`
);
} catch (error) {
logger.error('Unable to connect to the database;', error);
}
}
connectDatabase().then(() => {
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
});

View file

@ -1,5 +1,3 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('organization', {
@ -20,7 +18,7 @@ module.exports = {
});
},
async down(queryInterface, Sequelize) {
async down(queryInterface) {
await queryInterface.dropTable('organization');
},
};

View file

@ -1,5 +1,3 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('user', {
@ -36,7 +34,7 @@ module.exports = {
});
},
async down(queryInterface, Sequelize) {
async down(queryInterface) {
await queryInterface.dropTable('user');
},
};

View file

@ -1,5 +1,3 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('reset_request', {
@ -16,7 +14,7 @@ module.exports = {
});
},
async down(queryInterface, Sequelize) {
async down(queryInterface) {
await queryInterface.dropTable('reset_request');
},
};

View file

@ -1,5 +1,3 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('token', {
@ -19,7 +17,7 @@ module.exports = {
});
},
async down(queryInterface, Sequelize) {
async down(queryInterface) {
await queryInterface.dropTable('token');
},
};

View file

@ -1,5 +1,3 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn('organization', 'ownerID', {
@ -13,7 +11,7 @@ module.exports = {
});
},
async down(queryInterface, Sequelize) {
async down(queryInterface) {
await queryInterface.removeColumn('organization', 'ownerID');
},
};

View file

@ -1,5 +1,3 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('org_members', {
@ -22,7 +20,7 @@ module.exports = {
});
},
async down(queryInterface, Sequelize) {
async down(queryInterface) {
await queryInterface.dropTable('org_members');
},
};

View file

@ -1,5 +1,3 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn('reset_request', 'ownerID', {
@ -13,7 +11,7 @@ module.exports = {
});
},
async down(queryInterface, Sequelize) {
async down(queryInterface) {
await queryInterface.removeColumn('reset_request', 'ownerID');
},
};

View file

@ -1,5 +1,3 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn('token', 'ownerID', {
@ -13,7 +11,7 @@ module.exports = {
});
},
async down(queryInterface, Sequelize) {
async down(queryInterface) {
await queryInterface.removeColumn('token', 'ownerID');
},
};

View file

@ -29,7 +29,7 @@ module.exports = (sequelize, DataTypes) => {
allowNull: false,
},
},
{ sequelize, tableName: 'organization' }
{ sequelize, tableName: 'organization' },
);
return Organization;

View file

@ -18,7 +18,7 @@ module.exports = (sequelize, DataTypes) => {
defaultValue: DataTypes.UUIDV4,
},
},
{ sequelize, tableName: 'reset_request' }
{ sequelize, tableName: 'reset_request' },
);
return ResetRequest;

View file

@ -22,7 +22,7 @@ module.exports = (sequelize, DataTypes) => {
defaultValue: true,
},
},
{ sequelize, tableName: 'token' }
{ sequelize, tableName: 'token' },
);
return Token;

View file

@ -53,7 +53,7 @@ module.exports = (sequelize, DataTypes) => {
type: DataTypes.STRING,
},
},
{ sequelize, tableName: 'user' }
{ sequelize, tableName: 'user' },
);
return User;

View file

@ -2,9 +2,11 @@ require('dotenv').config({ path: `../.env.${process.env.NODE_ENV}` });
const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const basename = path.basename(__filename);
const env = process.env.NODE_ENV;
const config = require('../config/config')[env];
const db = {};
const sequelize = new Sequelize({
@ -13,18 +15,10 @@ const sequelize = new Sequelize({
});
fs.readdirSync(__dirname)
.filter((file) => {
return (
file.indexOf('.') !== 0 &&
file !== basename &&
file.slice(-3) === '.js'
);
})
.filter((file) => file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js')
.forEach((file) => {
const model = require(path.join(__dirname, file))(
sequelize,
Sequelize.DataTypes
);
// eslint-disable-next-line import/no-dynamic-require, global-require
const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);
db[model.name] = model;
});

View file

@ -15,7 +15,7 @@ async function basicAuth(req, res, next) {
// Make sure that a user exists with that email
const result = await User.findOne({
where: { email: email },
where: { email },
});
if (!result) {
return res.status(404).send(errors.NotFound);
@ -24,7 +24,7 @@ async function basicAuth(req, res, next) {
// Hash the password and verify that the user hash is identical
const hashedPassword = forge.md.sha512.create().update(password).digest().toHex();
if (hashedPassword != result.password) {
if (hashedPassword !== result.password) {
return res.status(403).send(errors.Forbidden);
}

View file

@ -3,7 +3,7 @@ const { Organization } = require('../db/models/index');
const errors = require('../config/error.json');
async function checkKnownOrg(req, res, next) {
const user = req.user;
const { user } = req;
const orgID = req.params.orgID || req.query.orgID || req.body.orgID;
// Get all organizations by the provided ID, with a list of members and owner
@ -28,7 +28,7 @@ async function checkKnownOrg(req, res, next) {
}
// Verify that the user who submitted the request exists as an owner or member
if (!result.owner && result.member.length == 0) {
if (!result.owner && result.member.length === 0) {
return res.status(403).send(errors.Forbidden);
}

View file

@ -3,15 +3,15 @@ const { User } = require('../db/models/index');
const errors = require('../config/error.json');
async function checkKnownUser(req, res, next) {
const user = req.user;
const { user } = req;
const userID = req.params.userID || req.query.userID || req.body.userID;
// Return result immediately if the user is querying themselves
if (user.id == userID) {
if (user.id === userID) {
return next();
}
// Find the queried user and the members of their owned organizations and owner/members of their member organizations
// Find the queried user and append all the users in adjacent organizations
const result = await User.findByPk(userID, {
include: [
{
@ -45,10 +45,10 @@ async function checkKnownUser(req, res, next) {
}
// Filter for an organization where the user is a member or an owner
const filteredResult = result.memberOrg.filter((org) => org.member.length != 0 || org.owner);
const filteredResult = result.memberOrg.filter((org) => org.member.length !== 0 || org.owner);
// Error if there is not an organization that includes that user and they are not in an owned organization
if (filteredResult.length == 0 && result.ownedOrg.length == 0) {
// Error if there is no relation between the users
if (filteredResult.length === 0 && result.ownedOrg.length === 0) {
return res.status(403).send(errors.Forbidden);
}

View file

@ -2,7 +2,7 @@ const formidable = require('formidable');
function parseFormData(req, res, next) {
// Parse multipart form data into formidable object and set static file save location
var form = formidable();
const form = formidable();
form.uploadDir = `${__dirname}/../public/`;
form.parse(req, (err, fields, files) => {

View file

@ -15,7 +15,7 @@ async function tokenAuth(req, res, next) {
const date = new Date();
if (!result) {
return res.status(401).send(errors.Unauthenticated);
} else if (result.expires && date.setDate(date.getDate + 1) > result.createdAt) {
} if (result.expires && date.setDate(date.getDate + 1) > result.createdAt) {
await Token.findOneAndDelete({ token });
return res.status(401).send(errors.Unauthenticated);
}

View file

@ -25,7 +25,8 @@
"sequelize": "^6.17.0",
"sequelize-cli": "^6.4.1",
"sqlite3": "^5.0.2",
"uuid": "^8.3.2"
"uuid": "^8.3.2",
"winston": "^3.8.2"
},
"devDependencies": {
"jest": "^27.5.1",

View file

@ -1,6 +1,6 @@
async function GetMemberOrgs(req, res, next) {
async function GetMemberOrgs(req, res) {
// Retrieve all of the organizations the user is a part of
const user = req.user;
const { user } = req;
const result = await user.getMemberOrg({
attributes: { exclude: ['updatedAt'] },

View file

@ -1,6 +1,6 @@
async function GetOwnedOrgs(req, res, next) {
async function GetOwnedOrgs(req, res) {
// Retrieve all organizations the user owns
const user = req.user;
const { user } = req;
const result = await user.getOwnedOrg({
attributes: { exclude: ['updatedAt'] },

View file

@ -1,7 +1,7 @@
const { User } = require('../../db/models/index');
async function GetUserDetails(req, res, next) {
const userID = req.params.userID;
async function GetUserDetails(req, res) {
const { userID } = req.params;
// Retrieve user while excluding sensitive information
const result = await User.findOne({

View file

@ -1,8 +1,8 @@
const { Token } = require('../../db/models/index');
async function Login(req, res, next) {
const user = req.user;
const remember = req.body.remember;
async function Login(req, res) {
const { user } = req;
const { remember } = req.body;
// Create token for user while setting expiry
await Token.destroy({ where: { ownerID: user.id } });

View file

@ -1,5 +1,5 @@
async function Logout(req, res, next) {
const user = req.user;
async function Logout(req, res) {
const { user } = req;
const token = await user.getToken();
if (token) {

View file

@ -3,8 +3,8 @@ const forge = require('node-forge');
const { User, Organization } = require('../../db/models/index');
const errors = require('../../config/error.json');
async function Register(req, res, next) {
const body = req.body;
async function Register(req, res) {
const { body } = req;
// Verify that the object body contains all SQL required fields
if (!body.firstName || !body.lastName || !body.email || !body.password) {
@ -32,7 +32,7 @@ async function Register(req, res, next) {
if (!org) {
// Create user regularly if organization is not found
const result = await User.create({
await User.create({
...body,
password: passwordHash,
});
@ -49,8 +49,6 @@ async function Register(req, res, next) {
return res.sendStatus(200);
} catch (e) {
console.log(e);
return res.status(500).send(errors.Generic);
}
} else {
@ -63,8 +61,6 @@ async function Register(req, res, next) {
return res.sendStatus(200);
} catch (e) {
console.log(e);
return res.status(500).send(errors.Generic);
}
}

View file

@ -1,5 +1,5 @@
async function Remember(req, res) {
const user = req.user;
const { user } = req;
return res.send({
user,

View file

@ -3,16 +3,16 @@ const { CourierClient } = require('@trycourier/courier');
const { User } = require('../../db/models/index');
const errors = require('../../config/error.json');
async function RequestReset(req, res, next) {
async function RequestReset(req, res) {
try {
// Make sure that the request contains an email
const email = req.body.email;
const { email } = req.body;
if (!email) {
return res.status(400).send(errors.Incomplete);
}
// Confirm that a user exists with that email
const result = await User.findOne({ where: { email: email } });
const result = await User.findOne({ where: { email } });
if (!result) {
return res.status(404).send(errors.NotFound);
}
@ -38,7 +38,6 @@ async function RequestReset(req, res, next) {
return res.sendStatus(200);
} catch (e) {
console.log(e);
return res.status(500).send(errors.Generic);
}
}

View file

@ -3,9 +3,9 @@ const forge = require('node-forge');
const { ResetRequest } = require('../../db/models/index');
const errors = require('../../config/error.json');
async function ResetPassword(req, res, next) {
async function ResetPassword(req, res) {
const reqID = req.params.id;
const password = req.body.password;
const { password } = req.body;
// Verify that the form contains a new password
if (!password) {

View file

@ -1,7 +1,7 @@
const fs = require('fs');
function SetAudio(req, res, next) {
const user = req.user;
function SetAudio(req, res) {
const { user } = req;
const oldLocation = req.files.audioFile.filepath;
@ -11,9 +11,7 @@ function SetAudio(req, res, next) {
const newLocation = `${uploadDir}/${fileName}`;
// Move file from public to folder
fs.rename(oldLocation, newLocation, (err) => {
if (err) console.log(err);
});
fs.rename(oldLocation, newLocation);
res.sendStatus(200);
}

View file

@ -3,7 +3,7 @@ const { Op } = require('sequelize');
const { User } = require('../../db/models/index');
const errors = require('../../config/error.json');
async function UpdateUserDetails(req, res, next) {
async function UpdateUserDetails(req, res) {
const { token, ...body } = req.body;
// Verify that the email is not a duplicate, if it exists

View file

@ -1,5 +1,6 @@
var express = require('express');
var router = express.Router();
const express = require('express');
const router = express.Router();
const parseFormData = require('../middleware/parseFormData');
const basicAuth = require('../middleware/basicAuth');

View file

@ -2,9 +2,9 @@ const { Organization } = require('../../db/models/index');
const errors = require('../../config/error.json');
async function AddOrgMember(req, res, next) {
const user = req.user;
const orgID = req.params.orgID;
async function AddOrgMember(req, res) {
const { user } = req;
const { orgID } = req.params;
// Verify that an organization exists with the ID provided
const result = await Organization.findByPk(orgID);

View file

@ -1,8 +1,8 @@
const errors = require('../../config/error.json');
async function CreateOrg(req, res, next) {
const user = req.user;
const orgName = req.body.orgName;
async function CreateOrg(req, res) {
const { user } = req;
const { orgName } = req.body;
// Error if the form does not contain an organization name
if (!orgName) {

View file

@ -2,9 +2,9 @@ const { Organization } = require('../../db/models/index');
const errors = require('../../config/error.json');
async function DeleteOrg(req, res, next) {
const user = req.user;
const orgID = req.params.orgID;
async function DeleteOrg(req, res) {
const { user } = req;
const { orgID } = req.params;
// Error if an organization id was not provided
if (!orgID) {

View file

@ -1,8 +1,8 @@
const { Organization } = require('../../db/models/index');
async function GetOrg(req, res, next) {
const user = req.user;
const orgID = req.params.orgID;
async function GetOrg(req, res) {
const { user } = req;
const { orgID } = req.params;
// Retrieve organization, owner, and members
const org = await Organization.findByPk(orgID, {
@ -27,7 +27,7 @@ async function GetOrg(req, res, next) {
return res.send({
org,
memberCount: org.member.length,
owner: org.ownerID == user.id,
owner: org.ownerID === user.id,
});
}

Some files were not shown because too many files have changed in this diff Show more