Skip to content

Commit

Permalink
Merge pull request #5 from tkhv/UI
Browse files Browse the repository at this point in the history
Added OAuth, CosmosDB, & Markdown Rendering
  • Loading branch information
Junhaeng7 committed Nov 24, 2023
2 parents ec20e0d + c414cdd commit e312741
Show file tree
Hide file tree
Showing 16 changed files with 1,403 additions and 173 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ AZURE_STORAGE_URL=""
AZURE_TENANT_ID=""
AZURE_CLIENT_ID=""
AZURE_CLIENT_SECRET=""
AZURE_COSMOSDB_PG_URL=""
AZURE_COSMOSDB_PG_USER=""
AZURE_COSMOSDB_PG_PASSWORD=""
AZURE_COSMOSDB_PG_DBNAME=""
AUTH_SECRET="" # Randomly generated string with `openssl rand -base64 32`
NEXTAUTH_URL="http://localhost:3000"
GITHUB_ID=""
GITHUB_SECRET=""
```

Then install and run the development server:
Expand Down
59 changes: 56 additions & 3 deletions app/(canvas)/courses/[courseName]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,59 @@
"use client";
import { useState } from "react";

export default function Course({ params }: { params: { courseName: string } }) {
return <div className="flex">course {params.courseName} Home </div>;
import React from "react";
const { BlockBlobClient } = require("@azure/storage-blob");
import ReactMarkdown from "react-markdown";

async function getSASurl(courseName: string) {
let res = await fetch(
"http://localhost:3000/api/" +
courseName +
"/getSAS?filename=readme/syllabus.md",
{
method: "GET",
}
);

if (!res.ok) {
throw new Error("Failed to fetch data");
}
let sasURL = await res.json();
return sasURL.sasURL;
}

// Convert stream to text
async function streamToText(readable: any) {
readable.setEncoding("utf8");
let data = "";
for await (const chunk of readable) {
data += chunk;
}
return data;
}

// Download the blob content
async function getHomePage(courseName: string) {
const sasURL: string = await getSASurl(courseName);
const blockBlobClient = new BlockBlobClient(sasURL);

const downloadBlockBlobResponse = await blockBlobClient.download(0);
const downloaded = await streamToText(
await downloadBlockBlobResponse.readableStreamBody
);
return downloaded;
}

export default async function Course({
params,
}: {
params: { courseName: string };
}) {
// const content = await getHomePage(params.courseName); FETCH DISABLED WHILE DEVELOPING

return (
<div>
{/* <ReactMarkdown className="prose" children={content} /> FETCH DISABLED WHILE DEVELOPING*/}
<h1>Course {params.courseName}</h1>
</div>
);
}
33 changes: 25 additions & 8 deletions app/(canvas)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,34 @@
"use client";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { auth } from "@/auth";
import { redirect } from "next/navigation";

import Navbar from "@/app/components/Navbar";
import { useState, createContext, use, SetStateAction, Dispatch } from "react";
import { useUserContext } from "../context/userContext";
import { CourseContextProvider } from "../context/courseContext";
import { CourseList } from "@/lib/types";
import { CourseList, User } from "@/lib/types";

export default function CanvasLayout({
export default async function CanvasLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await auth(); // This is from next-auth after OAuth login
if (!session) {
redirect("/api/auth/signin?callbackUrl=/dashboard");
return null;
}
if (!session) {
return;
}
const user: User = {
id: parseInt(session.user.id),
email: session.user.email || "",
name: session.user.name || "",
image: session.user.image || "",
};
// console.log(session?.user.name);
// console.log(session?.user.email);
// console.log(session?.user.image);
// console.log(session?.user.id);

/* courseList using useContext is reset when refreshing, so I commented it out for now.
maybe extract the courseList by using api call here would be better
*/
Expand All @@ -26,7 +43,7 @@ export default function CanvasLayout({
return (
<div className="flex">
<CourseContextProvider>
<Navbar courseList={courseList} />
<Navbar courseList={courseList} user={user} />
{children}
</CourseContextProvider>
</div>
Expand Down
26 changes: 5 additions & 21 deletions app/api/[containerName]/getSAS/route.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,13 @@
const { NextRequest } = require("next/server");
const { NextRequest, NextResponse } = require("next/server");

const {
BlobServiceClient,
ContainerClient,
BlobSASPermissions,
SASProtocol,
generateBlobSASQueryParameters,
} = require("@azure/storage-blob");
const { DefaultAzureCredential } = require("@azure/identity");

const getBlobServiceClient = async () => {
return await new BlobServiceClient(
process.env.AZURE_STORAGE_URL,
new DefaultAzureCredential()
);
};

const getContainerClient = async (containerName: string) => {
return await new ContainerClient(
process.env.AZURE_STORAGE_URL + `/${containerName}`,
new DefaultAzureCredential()
);
};

/* This GET endpoint is called by the client to get a SAS token for a specified blobName (filename).
The SAS token has a 10 minute expiry and has permissions to add, create, delete, read, and write
that specific blob. The client can then use the SAS token to authenticate with Azure Storage. */
Expand All @@ -32,7 +17,6 @@ export async function GET(
{ params }: { params: { containerName: string } }
) {
const blobName = request.nextUrl.searchParams.get("filename");
const requestedPermissions = request.nextUrl.searchParams.get("permissions");
const containerName = params.containerName;
const storageURL = process.env.AZURE_STORAGE_URL;
const accountName = process.env.AZURE_ACCOUNT_NAME;
Expand All @@ -43,13 +27,12 @@ export async function GET(
const TEN_MINUTES_BEFORE_NOW = new Date(NOW.valueOf() - TEN_MINUTES);
const TEN_MINUTES_AFTER_NOW = new Date(NOW.valueOf() + TEN_MINUTES);

const blobServiceClient = await new BlobServiceClient(
const blobServiceClient = new BlobServiceClient(
storageURL,
new DefaultAzureCredential()
);

// Container must already exist
const userDelegationKey = blobServiceClient.getUserDelegationKey(
const userDelegationKey = await blobServiceClient.getUserDelegationKey(
TEN_MINUTES_BEFORE_NOW,
TEN_MINUTES_AFTER_NOW
);
Expand All @@ -69,5 +52,6 @@ export async function GET(
accountName
).toString();

return new Response(sasToken);
const sasUrl = `${storageURL}/${containerName}/${blobName}?${sasToken}`;
return NextResponse.json({ sasURL: sasUrl });
}
3 changes: 3 additions & 0 deletions app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { handlers } from "@/auth";

export const { GET, POST } = handlers;
12 changes: 6 additions & 6 deletions app/components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
"use client";
import Link from "next/link";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
import { FC, useContext } from "react";
import { usePathname } from "next/navigation";
import { FC } from "react";
import { useCourseContext } from "../context/courseContext";
import { Course, CourseList } from "@/lib/types";
import { Course, CourseList, User } from "@/lib/types";

import { UserNav } from "./user-nav";

type NavbarProps = {
courseList: CourseList;
user: User;
};

const Navbar: FC<NavbarProps> = ({ courseList }) => {
const Navbar: FC<NavbarProps> = ({ courseList, user }) => {
const { currentCourse, setCurrentCourse } = useCourseContext();

const pathName = usePathname();
Expand All @@ -24,8 +25,7 @@ const Navbar: FC<NavbarProps> = ({ courseList }) => {
return (
<nav className="flex flex-col bg-navbarColor text-white h-screen">
<div className="p-4 mb-2 mt-4 flex flex-col items-center">
{/* Replace with logged in username */}
<UserNav username={"USERNAME"} />
<UserNav username={user.name} imageURL={user.image} />
</div>
<div className="flex flex-col flex-grow">
{/* Links */}
Expand Down
13 changes: 10 additions & 3 deletions app/components/user-nav.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Dialog, DialogTrigger } from "@/components/ui/dialog";
import {
Expand All @@ -12,7 +12,13 @@ import {
} from "@/components/ui/dropdown-menu";
import { AddCourseDialog } from "./AddCourseDialog";

export function UserNav({ username }: { username: string }) {
export function UserNav({
username,
imageURL,
}: {
username: string;
imageURL: string;
}) {
return (
<Dialog>
<DropdownMenu>
Expand All @@ -22,7 +28,8 @@ export function UserNav({ username }: { username: string }) {
className="relative h-8 w-8 rounded-full text-black"
>
<Avatar className="h-14 w-14">
<AvatarFallback>UN</AvatarFallback>
<AvatarImage src={imageURL} />
<AvatarFallback>GPT</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
Expand Down
19 changes: 0 additions & 19 deletions app/login/page.tsx

This file was deleted.

28 changes: 10 additions & 18 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
"use client";
import { useRouter } from "next/navigation";
import Navbar from "./components/Navbar";
import { useEffect, useState } from "react";
import { useUserContext } from "./context/userContext";
import { auth } from "@/auth";
import { redirect } from "next/navigation";

export default function Home() {
export default async function Home() {
// const { userId, setUserId, courseList, setCourseList } = useUserContext();
//this will be sent to navbar by using useContext

const router = useRouter();

useEffect(() => {
// user is not logged in
router.push("/login");
}, [router]);

return (
<div className="flex">
<button>Home</button>
</div>
);
const session = await auth();
if (!session) {
redirect("/api/auth/signin?callbackUrl=/dashboard");
return null;
} else {
redirect("/dashboard");
}
}
32 changes: 32 additions & 0 deletions auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import NextAuth from "next-auth";
import GithubProvider from "next-auth/providers/github";
import PostgresAdapter from "@auth/pg-adapter";
const { Pool } = require("pg");

// Store user accounts in CosmosDB Postgres as well
const pool = new Pool({
max: 300,
connectionTimeoutMillis: 5000,
host: process.env.AZURE_COSMOSDB_PG_URL,
port: 5432,
user: process.env.AZURE_COSMOSDB_PG_USER,
password: process.env.AZURE_COSMOSDB_PG_PASSWORD,
database: process.env.AZURE_COSMOSDB_PG_DBNAME,
ssl: true,
});

export const { handlers, auth } = NextAuth({
adapter: PostgresAdapter(pool),
providers: [
GithubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
],
callbacks: {
async session({ session, user }) {
session.user.id = user.id;
return session;
},
},
});
7 changes: 7 additions & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,10 @@ export type File = {
};

export type FilesList = File[];

export type User = {
id: number;
name: string;
email: string;
image: string;
};
Loading

0 comments on commit e312741

Please sign in to comment.