Skip to content

Commit

Permalink
Merge pull request #499 from ITPNYU/main
Browse files Browse the repository at this point in the history
Staging Release 11.13
  • Loading branch information
rlho authored Nov 13, 2024
2 parents 0b27a88 + 8006d4d commit 088c676
Show file tree
Hide file tree
Showing 18 changed files with 661 additions and 143 deletions.
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file

version: 2
updates:
- package-ecosystem: "" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
5 changes: 5 additions & 0 deletions .github/workflows/deploy_development_app_engine.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ jobs:
echo "NEXT_PUBLIC_DATABASE_NAME=${{ secrets.NEXT_PUBLIC_DATABASE_NAME }}" >> .env.production
echo "NEXT_PUBLIC_BRANCH_NAME=${{ secrets.NEXT_PUBLIC_BRANCH_NAME }}" >> .env.production
echo "NEXT_PUBLIC_GCP_LOG_NAME=${{ secrets.NEXT_PUBLIC_GCP_LOG_NAME }}" >> .env.production
echo "NYU_API_CLIENT_ID=${{ secrets.NYU_API_CLIENT_ID }}" >> .env.production
echo "NYU_API_CLIENT_SECRET=${{ secrets.NYU_API_CLIENT_SECRET }}" >> .env.production
echo "NYU_API_USER_NAME=${{ secrets.NYU_API_USER_NAME }}" >> .env.production
echo "NYU_API_PASSWORD=${{ secrets.NYU_API_PASSWORD }}" >> .env.production
echo "NYU_API_ACCESS_ID=${{ secrets.NYU_API_ACCESS_ID }}" >> .env.production
- name: Install dependencies
run: |
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/deploy_production_app_engine.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ jobs:
echo "NEXT_PUBLIC_DATABASE_NAME=${{ secrets.NEXT_PUBLIC_DATABASE_NAME }}" >> .env.production
echo "NEXT_PUBLIC_BRANCH_NAME=${{ secrets.NEXT_PUBLIC_BRANCH_NAME }}" >> .env.production
echo "NEXT_PUBLIC_GCP_LOG_NAME=${{ secrets.NEXT_PUBLIC_GCP_LOG_NAME }}" >> .env.production
echo "NYU_API_CLIENT_ID=${{ secrets.NYU_API_CLIENT_ID }}" >> .env.production
echo "NYU_API_CLIENT_SECRET=${{ secrets.NYU_API_CLIENT_SECRET }}" >> .env.production
echo "NYU_API_USER_NAME=${{ secrets.NYU_API_USER_NAME }}" >> .env.production
echo "NYU_API_PASSWORD=${{ secrets.NYU_API_PASSWORD }}" >> .env.production
echo "NYU_API_ACCESS_ID=${{ secrets.NYU_API_ACCESS_ID }}" >> .env.production
- name: Install dependencies
run: |
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/deploy_staging_app_engine.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ jobs:
echo "NEXT_PUBLIC_DATABASE_NAME=${{ secrets.NEXT_PUBLIC_DATABASE_NAME }}" >> .env.production
echo "NEXT_PUBLIC_BRANCH_NAME=${{ secrets.NEXT_PUBLIC_BRANCH_NAME }}" >> .env.production
echo "NEXT_PUBLIC_GCP_LOG_NAME=${{ secrets.NEXT_PUBLIC_GCP_LOG_NAME }}" >> .env.production
echo "NYU_API_CLIENT_ID=${{ secrets.NYU_API_CLIENT_ID }}" >> .env.production
echo "NYU_API_CLIENT_SECRET=${{ secrets.NYU_API_CLIENT_SECRET }}" >> .env.production
echo "NYU_API_USER_NAME=${{ secrets.NYU_API_USER_NAME }}" >> .env.production
echo "NYU_API_PASSWORD=${{ secrets.NYU_API_PASSWORD }}" >> .env.production
echo "NYU_API_ACCESS_ID=${{ secrets.NYU_API_ACCESS_ID }}" >> .env.production
- name: Install dependencies
run: |
Expand Down
21 changes: 21 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Security Policy

## Supported Versions

Use this section to tell people about which versions of your project are
currently being supported with security updates.

| Version | Supported |
| ------- | ------------------ |
| 5.1.x | :white_check_mark: |
| 5.0.x | :x: |
| 4.0.x | :white_check_mark: |
| < 4.0 | :x: |

## Reporting a Vulnerability

Use this section to tell people how to report a vulnerability.

Tell them where to go, how often they can expect to get an update on a
reported vulnerability, what to expect if the vulnerability is accepted or
declined, etc.
66 changes: 66 additions & 0 deletions booking-app/app/api/nyu/auth/token/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { TokenResponse } from "@/components/src/types";
import { NYUTokenManager } from "@/lib/server/nyuTokenCache";
import { Buffer } from "buffer";
import { NextResponse } from "next/server";

const NYU_AUTH_URL = "https://auth.nyu.edu/oauth2/token";

function getBasicAuthHeader(): string {
const clientId = process.env.NYU_API_CLIENT_ID;
const clientSecret = process.env.NYU_API_CLIENT_SECRET;

if (!clientId || !clientSecret) {
throw new Error("NYU credentials not configured");
}

const credentials = `${clientId}:${clientSecret}`;
return `Basic ${Buffer.from(credentials).toString("base64")}`;
}

export async function GET() {
try {
const tokenManager = NYUTokenManager.getInstance();
let tokenCache = await tokenManager.getToken();
if (!tokenCache) {
const username = process.env.NYU_API_USER_NAME;
const password = process.env.NYU_API_PASSWORD;

const params = new URLSearchParams({
grant_type: "password",
username,
password,
scope: "openid",
});

const response = await fetch(NYU_AUTH_URL, {
method: "POST",
headers: {
Authorization: getBasicAuthHeader(),
"Content-Type": "application/x-www-form-urlencoded",
},
body: params.toString(),
// @ts-ignore
rejectUnauthorized: false,
});

const tokenResponse: TokenResponse = await response.json();

tokenManager.setToken(
tokenResponse.access_token,
tokenResponse.expires_in,
tokenResponse.token_type,
);
tokenCache = await tokenManager.getToken()!;
}
return NextResponse.json({
isAuthenticated: true,
expiresAt: new Date(tokenCache.expires_at).toISOString(),
});
} catch (error) {
console.error("NYU Auth error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
}
70 changes: 70 additions & 0 deletions booking-app/app/api/nyu/identity/[uniqueId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { ensureNYUToken } from "@/lib/server/nyuApiAuth";
import { NYUTokenManager } from "@/lib/server/nyuTokenCache";
import { NextRequest, NextResponse } from "next/server";

const NYU_API_BASE = "https://api.nyu.edu/identity-v2-sys";

export async function GET(
request: NextRequest,
{ params }: { params: { uniqueId: string } },
) {
try {
const authResult = await ensureNYUToken();
if (!authResult.isAuthenticated || !authResult.token) {
return NextResponse.json(
{ error: authResult.error || "Authentication required" },
{ status: 401 },
);
}

const apiAccessId = process.env.NYU_API_ACCESS_ID;

if (!apiAccessId) {
return NextResponse.json(
{ error: "API access ID not configured" },
{ status: 500 },
);
}

const url = new URL(
`${NYU_API_BASE}/identity/unique-id/primary-affil/${params.uniqueId}`,
);
url.searchParams.append("api_access_id", apiAccessId);

const response = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${authResult.token}`,
Accept: "application/json",
},
});
console.log("response", response);

if (!response.ok) {
const errorText = await response.text();
console.error("NYU Identity API Error:", {
status: response.status,
body: errorText,
uniqueId: params.uniqueId,
});

if (response.status === 401) {
NYUTokenManager.getInstance().clearToken();
}

return NextResponse.json(
{ error: `NYU API call failed: ${response.status}` },
{ status: response.status },
);
}

const userData = await response.json();

return NextResponse.json(userData);
} catch (error) {
console.error("Identity API error:", error);
return NextResponse.json(
{ error: "Failed to fetch identity data" },
{ status: 500 },
);
}
}
Original file line number Diff line number Diff line change
@@ -1,35 +1,36 @@
import { Box, Button, Typography } from "@mui/material";
import {
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { SubmitHandler, useForm } from "react-hook-form";
import {
AttendeeAffiliation,
FormContextLevel,
Inputs,
Role,
UserApiData,
} from "../../../../types";
import {
BookingFormAgreementCheckbox,
BookingFormDropdown,
BookingFormSwitch,
BookingFormTextField,
} from "./BookingFormInputs";
import { Box, Button, Typography } from "@mui/material";
import React, {
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { SubmitHandler, useForm } from "react-hook-form";

import { BookingContext } from "../bookingProvider";
import BookingFormMediaServices from "./BookingFormMediaServices";
import BookingSelection from "./BookingSelection";
import { DatabaseContext } from "../../components/Provider";
import isEqual from "react-fast-compare";
import { styled } from "@mui/system";
import useCheckAutoApproval from "../hooks/useCheckAutoApproval";
import { useRouter } from "next/navigation";
import isEqual from "react-fast-compare";
import { DatabaseContext } from "../../components/Provider";
import { BookingContext } from "../bookingProvider";
import useCheckAutoApproval from "../hooks/useCheckAutoApproval";
import useSubmitBooking from "../hooks/useSubmitBooking";
import BookingFormMediaServices from "./BookingFormMediaServices";
import BookingSelection from "./BookingSelection";

const Section = ({ title, children }) => (
<div style={{ marginBottom: "20px" }}>
Expand Down Expand Up @@ -60,9 +61,14 @@ const Container = styled(Box)(({ theme }) => ({
interface Props {
calendarEventId?: string;
formContext: FormContextLevel;
userApiData?: UserApiData;
}

export default function FormInput({ calendarEventId, formContext }: Props) {
export default function FormInput({
calendarEventId,
formContext,
userApiData,
}: Props) {
const { userEmail, settings } = useContext(DatabaseContext);
const {
role,
Expand All @@ -77,12 +83,17 @@ export default function FormInput({ calendarEventId, formContext }: Props) {
const router = useRouter();
const registerEvent = useSubmitBooking(formContext);
const { isAutoApproval } = useCheckAutoApproval();
const getDefaultValue = (key: keyof UserApiData): string => {
if (!userApiData) return "";
return userApiData[key] || "";
};

const {
control,
handleSubmit,
trigger,
watch,
reset,
formState: { errors, isValid },
} = useForm<Inputs>({
defaultValues: {
Expand All @@ -102,6 +113,10 @@ export default function FormInput({ calendarEventId, formContext }: Props) {
bookingType: "",
secondaryName: "",
otherDepartment: "",
firstName: getDefaultValue("preferred_first_name"),
lastName: getDefaultValue("preferred_last_name"),
nNumber: getDefaultValue("university_id"),
netId: getDefaultValue("netid"),
...formData, // restore answers if navigating between form pages
// copy department + role from earlier in form
department,
Expand Down Expand Up @@ -177,7 +192,17 @@ export default function FormInput({ calendarEventId, formContext }: Props) {
},
[userEmail]
);

useEffect(() => {
if (userApiData) {
reset((formValues) => ({
...formValues,
firstName: userApiData.preferred_first_name || formValues.firstName,
lastName: userApiData.preferred_last_name || formValues.lastName,
nNumber: userApiData.university_id || formValues.nNumber,
netId: userApiData.netid || formValues.netId,
}));
}
}, [userApiData, reset]);
const disabledButton =
!(checklist && resetRoom && bookingPolicy && isValid) ||
isBanned ||
Expand Down Expand Up @@ -338,10 +363,7 @@ export default function FormInput({ calendarEventId, formContext }: Props) {
id="roomSetup"
label="Room Setup Needed?"
required={false}
description={
<p>
</p>
}
description={<p></p>}
{...{ control, errors, trigger }}
/>
{watch("roomSetup") === "yes" && (
Expand Down Expand Up @@ -413,10 +435,7 @@ export default function FormInput({ calendarEventId, formContext }: Props) {
<BookingFormSwitch
id="catering"
label="Catering?"
description={
<p>
</p>
}
description={<p></p>}
required={false}
{...{ control, errors, trigger }}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"use client";

import { FormContextLevel } from "@/components/src/types";
import FormInput from "../components/FormInput";
import { FormContextLevel, UserApiData } from "@/components/src/types";
import Grid from "@mui/material/Unstable_Grid2";
import React from "react";
import { useContext, useEffect, useState } from "react";
import { DatabaseContext } from "../../components/Provider";
import FormInput from "../components/FormInput";
import useCheckFormMissingData from "../hooks/useCheckFormMissingData";

interface Props {
Expand All @@ -15,12 +16,34 @@ export default function BookingFormDetailsPage({
calendarEventId,
formContext = FormContextLevel.FULL_FORM,
}: Props) {
const { netId } = useContext(DatabaseContext);
useCheckFormMissingData();
const [userApiData, setUserApiData] = useState<UserApiData | undefined>(
undefined
);
useEffect(() => {
const fetchUserData = async () => {
if (!netId) return;

try {
const response = await fetch(`/api/nyu/identity/${netId}`);

if (response.ok) {
const data = await response.json();
setUserApiData(data);
}
} catch (err) {
console.error("Failed to fetch user data:", err);
}
};

fetchUserData();
}, [netId]);
return (
<Grid container>
<Grid width={330} />
<Grid xs={12} md={7} margin={2} paddingRight={{ xs: 0, md: 2 }}>
<FormInput {...{ formContext, calendarEventId }} />
<FormInput {...{ formContext, calendarEventId, userApiData }} />
</Grid>
</Grid>
);
Expand Down
Loading

0 comments on commit 088c676

Please sign in to comment.