Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: Re-write database schema to be more relational #86

Merged
merged 1 commit into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ module.exports = {
rules: {
// formatting / style rules
"prettier/prettier": "warn",
"no-console": "error",
"no-console": ["error", { allow: ["error"] }],
camelcase: "warn",
"react/function-component-definition": [
"error",
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ For your convenience, there are many tools available to test out components, act

#### Testing accounts

`/test/account/page.tsx` is a page that can be accessed at `/test/account` and is used to make test accounts.
Fill out the enrollment form to create client accounts.

To create an admin account, you must set the `ADMIN_CREATION_KEY` environment variable to a secret key and provide it in the `x-api-key` header when making a POST request to `/api/users/actions/create-admin`.

#### Testing emails

Expand Down
69 changes: 69 additions & 0 deletions src/app/api/users/actions/create-admin-account/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { z } from "zod";

import { createAdminUser } from "@/server/api/users/private-mutations";

const adminUserRequestSchema = z.object({
firstName: z.string().min(1, { message: "First name is required" }),
lastName: z.string().min(1, { message: "Last name is required" }),
email: z.string().email({ message: "Invalid email" }),
password: z.string().min(8, {
message: "Password must be at least 8 characters",
}),
});

export type AdminUserRequest = z.infer<typeof adminUserRequestSchema>;

export async function POST(request: Request) {
try {
const json = await request.json();

const apiKeyHeader = request.headers.get("x-api-key");

if (!apiKeyHeader || apiKeyHeader !== process.env.ADMIN_CREATION_KEY) {
return new Response(
JSON.stringify({ success: false, error: "Invalid request." }),
{
status: 401,
headers: { "Content-Type": "application/json" },
},
);
}

const parsedJson = adminUserRequestSchema.safeParse(json);

if (parsedJson.success === false) {
return new Response(
JSON.stringify({ success: false, error: "Invalid request body." }),
{
status: 400,
headers: { "Content-Type": "application/json" },
},
);
}

const adminUserRequest = parsedJson.data;

const [, error] = await createAdminUser(adminUserRequest);

if (error !== null) {
return new Response(JSON.stringify({ success: false, error: error }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}

return new Response(JSON.stringify({ success: true }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error(error);
return new Response(
JSON.stringify({ success: false, error: "Internal server error." }),
{
status: 500,
headers: { "Content-Type": "application/json" },
},
);
}
}
11 changes: 6 additions & 5 deletions src/app/dashboard/client/healthy-habits/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { Box, Typography } from "@mui/material";
import { redirect } from "next/navigation";

import HealthyHabits from "@/components/ClientDashboard/HealthyHabits";
import { getHealthyHabitsTrackingFormsByEmail } from "@/server/api/healthy-habits-tracking-forms/queries";
import { getUserByEmail } from "@/server/api/users/queries";
import { ClientUser } from "@/types";
import getUserSession from "@/utils/getUserSession";

export default async function HealthyHabitsPage() {
Expand All @@ -12,9 +13,9 @@ export default async function HealthyHabitsPage() {
redirect("/");
}

const [trackingForms, error] = await getHealthyHabitsTrackingFormsByEmail(
session.user.email,
);
const [user, error] = await getUserByEmail(session.user.email, {
populateHealthyHabitsTrackingForms: true,
});

if (error !== null) {
return (
Expand Down Expand Up @@ -45,7 +46,7 @@ export default async function HealthyHabitsPage() {
padding: 1,
}}
>
<HealthyHabits email={session.user.email} trackingForms={trackingForms} />
<HealthyHabits user={user as ClientUser} />
</Box>
);
}
13 changes: 9 additions & 4 deletions src/app/dashboard/client/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import Link from "next/link";
import { redirect } from "next/navigation";

import EnrolledProgramsSelectionScreen from "@/components/ClientDashboard/EnrolledProgramSelectionScreen.tsx";
import { getClientActivePrograms } from "@/server/api/program-enrollments/queries";
import { getUserByEmail } from "@/server/api/users/queries";
import { ClientUser } from "@/types";
import getUserSession from "@/utils/getUserSession";

export default async function ClientDashboardPage() {
Expand All @@ -13,9 +14,9 @@ export default async function ClientDashboardPage() {
redirect("/");
}

const clientEmail = session.user.email;
const [programEnrollments, error] =
await getClientActivePrograms(clientEmail);
const [user, error] = await getUserByEmail(session.user.email, {
populateProgramEnrollments: true,
});

if (error !== null) {
return (
Expand All @@ -35,6 +36,10 @@ export default async function ClientDashboardPage() {
);
}

const programEnrollments = (user as ClientUser).programEnrollments.filter(
(programEnrollment) => programEnrollment.status === "accepted",
);

if (programEnrollments.length === 0) {
return (
<Box
Expand Down
4 changes: 1 addition & 3 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Box, Button, Typography } from "@mui/material";
import { Box, Button } from "@mui/material";
import Link from "next/link";
import { redirect } from "next/navigation";

Expand All @@ -16,8 +16,6 @@ export default async function Home() {
case "client":
redirect("/dashboard/client");
break;
default:
return <Typography>Invalid role: {session.user.role}</Typography>;
}
}

Expand Down
3 changes: 0 additions & 3 deletions src/app/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Typography } from "@mui/material";
import { redirect } from "next/navigation";

import getUserSession from "@/utils/getUserSession";
Expand All @@ -17,7 +16,5 @@ export default async function SettingsPage() {
case "client":
redirect("/settings/client");
break;
default:
return <Typography>Invalid role: {session.user.role}</Typography>;
}
}
152 changes: 0 additions & 152 deletions src/app/test/account/page.tsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,28 @@ import React, { Dispatch, SetStateAction } from "react";

import { Row } from "@/components/AdminDashboard/PendingApplicationDashboard";
import { handleApproveProgramApplication } from "@/server/api/program-enrollments/public-mutations";
import { Program } from "@/types";
import { ProgramEnrollment } from "@/types";

type AcceptPendingApplicationButtonProps = {
email: string;
firstName: string;
program: Program;
programEnrollment: ProgramEnrollment;
rows: Row[];
setRows: Dispatch<SetStateAction<Row[]>>;
setSnackbarOpen: Dispatch<SetStateAction<boolean>>;
setSnackbarMessage: Dispatch<SetStateAction<string>>;
};

export default function AcceptPendingApplicationButton({
email,
firstName,
program,
programEnrollment,
rows,
setRows,
setSnackbarOpen,
setSnackbarMessage,
}: AcceptPendingApplicationButtonProps) {
const removePendingApplicationFromRows = () => {
const rowsWithoutProgramEnrollment = rows.filter(
(row) => row.email !== email || row.program !== program,
(row) =>
row.email !== programEnrollment.user.email ||
row.program !== programEnrollment.program,
);
setRows(rowsWithoutProgramEnrollment);
};
Expand All @@ -44,7 +42,7 @@ export default function AcceptPendingApplicationButton({
}

removePendingApplicationFromRows();
await handleApproveProgramApplication(email, firstName, program);
await handleApproveProgramApplication(programEnrollment);
setSnackbarMessage("Application successfully approved");
setSnackbarOpen(true);
};
Expand Down
Loading
Loading