Skip to content
This repository has been archived by the owner on Sep 12, 2023. It is now read-only.

Commit

Permalink
Merge pull request #757 from Northeastern-Electric-Racing/#730-add-us…
Browse files Browse the repository at this point in the history
…er-settings

#730 add user settings
  • Loading branch information
anthonybernardi authored Jul 27, 2022
2 parents cc01dca + 80b5803 commit c834862
Show file tree
Hide file tree
Showing 34 changed files with 678 additions and 76 deletions.
73 changes: 68 additions & 5 deletions src/backend/functions/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,33 @@ import {
buildNotFoundResponse,
buildSuccessResponse,
routeMatcher,
User
User,
AuthenticatedUser
} from 'utils';

const prisma = new PrismaClient();

const authUserQueryArgs = Prisma.validator<Prisma.UserArgs>()({
include: {
userSettings: true
}
});

const authenticatedUserTransformer = (
user: Prisma.UserGetPayload<typeof authUserQueryArgs>
): AuthenticatedUser => {
return {
userId: user.userId,
firstName: user.firstName,
lastName: user.lastName,
googleAuthId: user.googleAuthId,
email: user.email,
emailId: user.emailId,
role: user.role,
defaultTheme: user.userSettings?.defaultTheme
};
};

const usersTransformer = (user: Prisma.UserGetPayload<null>): User => {
if (user === null) throw new TypeError('User not found');

Expand Down Expand Up @@ -70,7 +92,10 @@ const logUserIn: ApiRouteFunction = async (_params, event) => {
if (!payload) throw new Error('Auth server response payload invalid');
const { sub: userId } = payload; // google user id
// check if user is already in the database via Google ID
let user = await prisma.user.findUnique({ where: { googleAuthId: userId } });
let user = await prisma.user.findUnique({
where: { googleAuthId: userId },
...authUserQueryArgs
});

// if not in database, create user in database
if (user === null) {
Expand All @@ -83,8 +108,10 @@ const logUserIn: ApiRouteFunction = async (_params, event) => {
lastName: payload['family_name']!,
googleAuthId: userId,
email: payload['email']!,
emailId
}
emailId,
userSettings: { create: {} }
},
...authUserQueryArgs
});
user = createdUser;
}
Expand All @@ -97,7 +124,33 @@ const logUserIn: ApiRouteFunction = async (_params, event) => {
}
});

return buildSuccessResponse(usersTransformer(user));
return buildSuccessResponse(authenticatedUserTransformer(user));
};

/** Get settings for the specified user */
const getUserSettings: ApiRouteFunction = async (params: { id: string }) => {
const userId: number = parseInt(params.id);
const settings = await prisma.user_Settings.upsert({
where: { userId },
update: {},
create: { userId }
});
if (!settings) return buildNotFoundResponse('settings for user', `#${params.id}`);
return buildSuccessResponse(settings);
};

/** Update settings for the specified user */
const updateUserSettings: ApiRouteFunction = async (params: { id: string }, event) => {
const userId: number = parseInt(params.id);
if (!event.body) return buildClientFailureResponse('No settings found to update.');
const body = JSON.parse(event.body!);
if (!body.defaultTheme) return buildClientFailureResponse('No defaultTheme found for settings.');
await prisma.user_Settings.upsert({
where: { userId },
update: { defaultTheme: body.defaultTheme },
create: { userId, defaultTheme: body.defaultTheme }
});
return buildSuccessResponse({ message: `Successfully updated settings for user ${userId}.` });
};

// Define all valid routes for the endpoint
Expand All @@ -116,6 +169,16 @@ const routes: ApiRoute[] = [
path: `${API_URL}${apiRoutes.USERS_LOGIN}`,
httpMethod: 'POST',
func: logUserIn
},
{
path: `${API_URL}${apiRoutes.USER_SETTINGS_BY_USER_ID}`,
httpMethod: 'GET',
func: getUserSettings
},
{
path: `${API_URL}${apiRoutes.USER_SETTINGS_BY_USER_ID}`,
httpMethod: 'POST',
func: updateUserSettings
}
];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- CreateEnum
CREATE TYPE "Theme" AS ENUM ('LIGHT', 'DARK');

-- CreateTable
CREATE TABLE "User_Settings" (
"id" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
"defaultTheme" "Theme" NOT NULL DEFAULT E'DARK',

PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "User_Settings.userId_unique" ON "User_Settings"("userId");

-- AddForeignKey
ALTER TABLE "User_Settings" ADD FOREIGN KEY ("userId") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE;
29 changes: 22 additions & 7 deletions src/backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ enum Role {
GUEST
}

enum Theme {
LIGHT
DARK
}

enum Scope_CR_Why_Type {
ESTIMATION
SCHOOL
Expand All @@ -46,13 +51,16 @@ enum Scope_CR_Why_Type {
}

model User {
userId Int @id @default(autoincrement())
firstName String
lastName String
googleAuthId String @unique
email String @unique
emailId String? @unique
role Role @default(GUEST)
userId Int @id @default(autoincrement())
firstName String
lastName String
googleAuthId String @unique
email String @unique
emailId String? @unique
role Role @default(GUEST)
userSettings User_Settings?
// Relation references
submittedChangeRequests Change_Request[] @relation(name: "submittedChangeRequests")
reviewedChangeRequests Change_Request[] @relation(name: "reviewedChangeRequests")
markedAsProjectLead Activation_CR[] @relation(name: "markAsProjectLead")
Expand All @@ -74,6 +82,13 @@ model Session {
deviceInfo String?
}

model User_Settings {
id String @id @default(uuid())
userId Int @unique
user User @relation(fields: [userId], references: [userId])
defaultTheme Theme @default(DARK)
}

model Change_Request {
crId Int @id @default(autoincrement())
submitterId Int
Expand Down
12 changes: 8 additions & 4 deletions src/backend/prisma/seed-data/risks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@

const dbSeedRisk1: any = {
projectId: 1,
detail: 'This one might be a bit too expensive',
createdByUserId: 1
createdByUserId: 1,
fields: {
detail: 'This one might be a bit too expensive'
}
};

const dbSeedRisk2: any = {
projectId: 1,
detail: 'Risky Risk 123',
createdByUserId: 1
createdByUserId: 1,
fields: {
detail: 'Risky Risk 123'
}
};

export const dbSeedAllRisks: any[] = [dbSeedRisk1, dbSeedRisk2];
4 changes: 2 additions & 2 deletions src/backend/prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const prisma = new PrismaClient();

const performSeed: () => Promise<void> = async () => {
for (const seedUser of dbSeedAllUsers) {
await prisma.user.create({ data: { ...seedUser } });
await prisma.user.create({ data: { ...seedUser, userSettings: { create: {} } } });
}

for (const seedSession of dbSeedAllSessions) {
Expand Down Expand Up @@ -44,7 +44,7 @@ const performSeed: () => Promise<void> = async () => {
data: {
createdBy: { connect: { userId: seedRisk.createdByUserId } },
project: { connect: { projectId: seedRisk.projectId } },
...seedRisk
...seedRisk.fields
}
});
}
Expand Down
14 changes: 14 additions & 0 deletions src/frontend/app/app-context/app-context.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ jest.mock('../app-context-auth/app-context-auth', () => {
};
});

jest.mock('../app-context-theme/app-context-theme', () => {
return {
__esModule: true,
default: (props: any) => {
return <div>app context theme {props.children}</div>;
}
};
});

// Sets up the component under test with the desired values and renders it
const renderComponent = () => {
render(
Expand All @@ -44,6 +53,11 @@ describe('app context', () => {
expect(screen.getByText('app context auth')).toBeInTheDocument();
});

it('renders the app context theme component', () => {
renderComponent();
expect(screen.getByText('app context theme')).toBeInTheDocument();
});

it('renders the app context text', () => {
renderComponent();
expect(screen.getByText('full context')).toBeInTheDocument();
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/app/app-public/app-public.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const AppPublic: React.FC = () => {
document.body.style.backgroundColor = theme.bgColor;

return (
<html className={theme.name}>
<html className={theme.className}>
<Switch>
<Route path={routes.LOGIN}>
<Login
Expand Down
12 changes: 0 additions & 12 deletions src/frontend/layouts/nav-top-bar/nav-top-bar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,9 @@
* See the LICENSE file in the repository root folder for details.
*/

import { useTheme } from '../../../services/theme.hooks';
import themes from '../../../shared/themes';
import { Theme } from '../../../shared/types';
import { render, routerWrapperBuilder, screen } from '../../../test-support/test-utils';
import NavTopBar from './nav-top-bar';

jest.mock('../../../services/theme.hooks');
const mockTheme = useTheme as jest.Mock<Theme>;

const mockHook = () => {
mockTheme.mockReturnValue(themes[0]);
};

/**
* Sets up the component under test with the desired values and renders it.
*/
Expand All @@ -29,8 +19,6 @@ const renderComponent = () => {
};

describe('navigation top bar tests', () => {
beforeEach(() => mockHook());

it('renders site title', () => {
renderComponent();
expect(screen.getByText(/NER PM Dashboard/i)).toBeInTheDocument();
Expand Down
17 changes: 1 addition & 16 deletions src/frontend/layouts/nav-top-bar/nav-top-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,17 @@
* See the LICENSE file in the repository root folder for details.
*/

import { Dropdown, Nav, Navbar } from 'react-bootstrap';
import { Nav, Navbar } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import { routes } from '../../../shared/routes';
import { useAuth } from '../../../services/auth.hooks';
import { fullNamePipe } from '../../../shared/pipes';
import { useTheme } from '../../../services/theme.hooks';
import themes from '../../../shared/themes';
import NavUserMenu from './nav-user-menu/nav-user-menu';
import NavNotificationsMenu from './nav-notifications-menu/nav-notifications-menu';
import styles from './nav-top-bar.module.css';

const NavTopBar: React.FC = () => {
const auth = useAuth();
const theme = useTheme();

return (
<Navbar className={styles.mainBackground} variant="light" expand="md" fixed="top">
Expand All @@ -33,18 +30,6 @@ const NavTopBar: React.FC = () => {
<Navbar.Toggle aria-controls="nav-top-bar-items" />
<Navbar.Collapse id="nav-top-bar-items">
<Nav className="ml-auto">
<Dropdown className={styles.dropdown}>
<Dropdown.Toggle variant={theme.cardBg}>{theme.name}</Dropdown.Toggle>
<Dropdown.Menu>
{themes
.filter((t) => t.name !== theme.name)
.map((t) => (
<Dropdown.Item key={t.name} onClick={() => theme.toggleTheme!(t.name)}>
{t.name}
</Dropdown.Item>
))}
</Dropdown.Menu>
</Dropdown>
<NavNotificationsMenu />
<div className={styles.username}>{fullNamePipe(auth.user)}</div>
<NavUserMenu />
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/pages/LoginPage/login-page/login-page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* See the LICENSE file in the repository root folder for details.
*/

import themes from '../../../../shared/themes';
import { render, screen } from '../../../../test-support/test-utils';
import LoginPage from './login-page';

Expand All @@ -16,6 +17,7 @@ const renderComponent = () => {
devFormSubmit={(e) => e}
prodSuccess={(r) => r}
prodFailure={(r) => r}
theme={themes[0]}
/>
);
};
Expand Down
8 changes: 4 additions & 4 deletions src/frontend/pages/LoginPage/login-page/login-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import GoogleLogin from 'react-google-login';
import { Card } from 'react-bootstrap';
import LoginDev from '../login-dev/login-dev';
import { useTheme } from '../../../../services/theme.hooks';
import { Theme } from '../../../../shared/types';

const styles = {
card: {
Expand All @@ -19,6 +19,7 @@ interface LoginPageProps {
devFormSubmit: (e: any) => any;
prodSuccess: (res: any) => any;
prodFailure: (res: any) => any;
theme: Theme;
}

/**
Expand All @@ -28,10 +29,9 @@ const LoginPage: React.FC<LoginPageProps> = ({
devSetRole,
devFormSubmit,
prodSuccess,
prodFailure
prodFailure,
theme
}) => {
const theme = useTheme();

return (
<Card bg={theme.cardBg} className={'mx-auto mt-sm-5 '} style={styles.card}>
<Card.Body>
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/pages/LoginPage/login.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const renderComponent = () => {
history.listen((loc) => {
pushed.push(loc.pathname);
});
return <Login postLoginRedirect={routes.HOME} />;
return <Login postLoginRedirect={{ url: routes.HOME, search: '' }} />;
};
const RouterWrapper = routerWrapperBuilder({ path: routes.LOGIN, route: routes.LOGIN });
return render(
Expand Down
Loading

0 comments on commit c834862

Please sign in to comment.