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(videos): integration fixes for video appointment upload flow #393

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from 9 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
57 changes: 0 additions & 57 deletions app/web/__tests__/dummy-authenticator.auth.test.ts

This file was deleted.

8 changes: 4 additions & 4 deletions app/web/db/sample/user.properties.csv
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
id,email,firstname,lastname,username,password,role
1,testuser1@gmail.com,user1,test,testuser1,password,CLIENT
2,testuser@gmail.com,user2,test,testuser2,password,CLIENT
3,testuser3@gmail.com,user3,test,testuser3,password,PROFESSIONAL
4,testuser4@gmail.com,user4,test,testuser4,password,PROFESSIONAL
1,testuser1@gmail.com,user1,test,testuser1,$2a$10$bMG7qmKpKQJ4DfbW0gA9HOqXo7ZzYT0c0bBe7FVZQUgvj9oIZrMwa,CLIENT
2,testuser@gmail.com,user2,test,testuser2,$2a$10$bMG7qmKpKQJ4DfbW0gA9HOqXo7ZzYT0c0bBe7FVZQUgvj9oIZrMwa,CLIENT
3,testuser3@gmail.com,user3,test,testuser3,$2a$10$bMG7qmKpKQJ4DfbW0gA9HOqXo7ZzYT0c0bBe7FVZQUgvj9oIZrMwa,PROFESSIONAL
4,testuser4@gmail.com,user4,test,testuser4,$2a$10$bMG7qmKpKQJ4DfbW0gA9HOqXo7ZzYT0c0bBe7FVZQUgvj9oIZrMwa,PROFESSIONAL
2 changes: 1 addition & 1 deletion app/web/db/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ model User {
id Int @id @default(autoincrement())
username String @unique
password String
email String
email String @unique
tthvo marked this conversation as resolved.
Show resolved Hide resolved
firstname String
lastname String
role Role
Expand Down
133 changes: 130 additions & 3 deletions app/web/src/app/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

"use server";

import { ViewableAppointment } from "@lib/appointment";
import {
PrivacyPalAuthUser,
getAuthManager,
Expand Down Expand Up @@ -44,6 +45,66 @@ const actionLog = (...args: any) => {
const allUsers = () => db.user.findMany();
const oneUser = (id: number) => db.user.findUnique({ where: { id } });

/**
*
* @param attributes
* @returns
*/
export async function findOneUserBy(attributes: {
id?: number;
username?: string;
email?: string;
}) {
try {
const { id, username, email } = attributes;
const user = await db.user.findUnique({ where: { id, username, email } });
return user;
} catch (err: any) {
console.error(err);
}
return null;
}

/**
*
* @param email the email of the user to find
* @returns a User object or null if no user is found with that email
*/
export async function findUserByEmail(email: string) {
return findOneUserBy({ email });
}

/**
* Finds exactly one user by id or null otherwise
* @param id the id of the user to find
* @returns a User object or null if no user is found with that id
*/
export async function findUserById(id: number) {
return findOneUserBy({ id });
}

/**
* Finds exactly one user by id and removes the password field from the returned object
* @param id the id of the user to find
* @returns a User object with the password field removed
*/
export async function findUserSanitizedById(
id: number,
): Promise<Omit<User, "password"> | null> {
tthvo marked this conversation as resolved.
Show resolved Hide resolved
const user = await db.user.findUnique({
where: { id },
select: {
id: true,
username: true,
firstname: true,
lastname: true,
email: true,
role: true,
},
});
return user;
}

/**
* Get all users from the database
*/
Expand All @@ -66,7 +127,7 @@ export async function getUserAppointments(user: User) {
},
});

const appointmentsWithUsers = await Promise.all(
const appointmentsWithUsers: ViewableAppointment[] = await Promise.all(
appointments.map(async (appointment) => {
const client: User | null = await db.user.findUnique({
where: { id: appointment.clientId },
Expand All @@ -76,9 +137,9 @@ export async function getUserAppointments(user: User) {
});

return {
professional: `${professional?.firstname} ${professional?.lastname}`,
client: `${client?.firstname} ${client?.lastname}`,
...appointment,
clientUser: client,
professionalUser: professional,
};
}),
);
Expand Down Expand Up @@ -184,6 +245,7 @@ export async function logIn(
revalidatePath("/", "layout");
redirect(redirectTo ?? "/");
}
throw new Error("Invalid credentials");
}

export async function logOut(redirectTo?: string) {
Expand Down Expand Up @@ -263,6 +325,39 @@ export async function getAppointmentsProfessional(professional: User) {
return appointments;
}

export async function getClientAppointment(
client: User,
apptId: number,
): Promise<Appointment | null> {
if (client.role !== Role.CLIENT) throw new Error("User is not a client");

const appointment = await db.appointment.findUnique({
where: {
id: apptId,
clientId: client.id,
tthvo marked this conversation as resolved.
Show resolved Hide resolved
},
});

return appointment;
}

export async function getProfessionalAppointment(
professional: User,
apptId: number,
) {
if (professional.role !== Role.PROFESSIONAL)
throw new Error("User is not a professional");

const appointment = await db.appointment.findUnique({
where: {
id: apptId,
proId: professional.id,
tthvo marked this conversation as resolved.
Show resolved Hide resolved
},
});

return appointment;
}

export async function getAppointmentsClient(client: User) {
if (client.role !== Role.CLIENT)
throw new Error("User is not a professional");
Expand All @@ -275,3 +370,35 @@ export async function getAppointmentsClient(client: User) {

return appointments;
}

/**
* This function checks if the user is in the appointment (and is therefore allowed to view it).
* @param apptId the appointment to check
* @returns true if the user is in this appointment, false otherwise
*/
export async function userBelongsToAppointment(
apptId: number | string,
): Promise<boolean> {
const user = await getLoggedInUser();
if (!user) return false;

if (typeof apptId === "string") apptId = parseInt(apptId);

const appointment = await db.appointment.findUnique({
where: {
id: apptId,
OR: [
{
clientId: user.id,
},
{
proId: user.id,
},
],
},
});

if (!appointment) return false;

return true;
}
2 changes: 2 additions & 0 deletions app/web/src/app/api/video/upload/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,5 @@ async function saveVideo(
throw err;
}
}

async function updateDatabase(appointmentId: number, resourceURI: string) {}
66 changes: 66 additions & 0 deletions app/web/src/app/staff/appointment/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright [2023] [Privacypal Authors]
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
findUserById,
findUserSanitizedById,
getLoggedInUser,
getProfessionalAppointment,
} from "@app/actions";
import AppointmentViewer from "@components/appointment/AppointmentViewer";
import { ViewableAppointment } from "@lib/appointment";
import { NextPageProps } from "@lib/url";
import { notFound, redirect } from "next/navigation";

export default async function AppointmentPage({ searchParams }: NextPageProps) {
// if no appointment id, redirect to staff dashboard
if (!searchParams?.id || Array.isArray(searchParams?.id)) {
return redirect("/staff/");
}

try {
// parseInt will throw an error if the id is not a number-string
const apptId = parseInt(searchParams?.id);
const user = await getLoggedInUser();

// if somehow there is no user, redirect to login
if (!user) redirect("/login");
tthvo marked this conversation as resolved.
Show resolved Hide resolved

// this function will throw an error if the logged in user is not a professional
const appt = await getProfessionalAppointment(user, apptId);

// if there is no appointment by that id, return 404
if (!appt) return notFound();

// display appointment data
const client = await findUserSanitizedById(appt.clientId);

if (!client) notFound();

const viewableAppointment: ViewableAppointment = {
id: appt.id,
clientUser: client,
professionalUser: user,
time: appt.time,
};

return (
<AppointmentViewer appointment={viewableAppointment} viewer={user} />
);
} catch (error: any) {
// return 404 if there is an error
return notFound();
}
}
45 changes: 39 additions & 6 deletions app/web/src/app/upload/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,45 @@
* limitations under the License.
*/

import { getClientAppointment, getLoggedInUser } from "@app/actions";
import { UploadVideoForm } from "@components/upload/UploadVideoForm";
import { NextPageProps } from "@lib/url";
import { notFound, redirect } from "next/navigation";

export default function Page() {
return (
<main>
<UploadVideoForm />
</main>
);
export default async function Page({ searchParams }: NextPageProps) {
// if no appointment id, redirect to staff dashboard
if (!searchParams?.id || Array.isArray(searchParams?.id)) {
return (
<main>
<div>
<h2>Not Found</h2>
<p>No appointment ID provided.</p>
</div>
</main>
);
}
try {
// parseInt will throw an error if the id is not a number-string
const apptId = parseInt(searchParams?.id);
const user = await getLoggedInUser();

// if somehow there is no user, redirect to login
if (!user) redirect("/login");

// this function will throw an error if the logged in user is not a professional
const appt = await getClientAppointment(user, apptId);

// if there is no appointment by that id, return 404
if (!appt) return notFound();

return (
<main>
<UploadVideoForm />
</main>
);
} catch (error: any) {
// return 404 if there is an error
// console.error(error);
return notFound();
}
}
Loading
Loading