diff --git a/www/README.md b/www/README.md index f5f87643..c92df561 100644 --- a/www/README.md +++ b/www/README.md @@ -79,7 +79,7 @@ This data is then returned from the `useWebRTC` hook and can be used in your com To generate the TypeScript files from the openapi.json file, make sure the python server is running, then run: ``` -openapi-generator-cli generate -i http://localhost:1250/openapi.json -g typescript-fetch -o app/api +yarn openapi ``` You may need to run `yarn global add @openapitools/openapi-generator-cli` first. You also need a Java runtime installed on your machine. diff --git a/www/app/(auth)/fiefWrapper.tsx b/www/app/(auth)/fiefWrapper.tsx new file mode 100644 index 00000000..187fef7c --- /dev/null +++ b/www/app/(auth)/fiefWrapper.tsx @@ -0,0 +1,11 @@ +"use client"; + +import { FiefAuthProvider } from "@fief/fief/nextjs/react"; + +export default function FiefWrapper({ children }) { + return ( + + {children} + + ); +} diff --git a/www/app/(auth)/userInfo.tsx b/www/app/(auth)/userInfo.tsx new file mode 100644 index 00000000..d0a8ad02 --- /dev/null +++ b/www/app/(auth)/userInfo.tsx @@ -0,0 +1,43 @@ +"use client"; +import { + useFiefIsAuthenticated, + useFiefUserinfo, +} from "@fief/fief/nextjs/react"; +import Link from "next/link"; +import Image from "next/image"; + +export default function UserInfo() { + const isAuthenticated = useFiefIsAuthenticated(); + const userinfo = useFiefUserinfo(); + + return ( +
+ {/* Logo on the left */} + + Reflector + + + {/* Text link on the right */} + {!isAuthenticated && ( + + Log in or create account + + )} + {isAuthenticated && ( + + {userinfo?.email} ( + + Log out + + ) + + )} +
+ ); +} diff --git a/www/app/api/.openapi-generator/FILES b/www/app/api/.openapi-generator/FILES index c6dc9bb8..91888ece 100644 --- a/www/app/api/.openapi-generator/FILES +++ b/www/app/api/.openapi-generator/FILES @@ -9,6 +9,7 @@ models/PageGetTranscript.ts models/RtcOffer.ts models/TranscriptTopic.ts models/UpdateTranscript.ts +models/UserInfo.ts models/ValidationError.ts models/index.ts runtime.ts diff --git a/www/app/api/apis/DefaultApi.ts b/www/app/api/apis/DefaultApi.ts index 147fd770..947c64ed 100644 --- a/www/app/api/apis/DefaultApi.ts +++ b/www/app/api/apis/DefaultApi.ts @@ -55,6 +55,10 @@ export interface V1TranscriptGetAudioRequest { transcriptId: any; } +export interface V1TranscriptGetAudioMp3Request { + transcriptId: any; +} + export interface V1TranscriptGetTopicsRequest { transcriptId: any; } @@ -159,6 +163,14 @@ export class DefaultApi extends runtime.BaseAPI { const headerParameters: runtime.HTTPHeaders = {}; + if (this.configuration && this.configuration.accessToken) { + // oauth required + headerParameters["Authorization"] = await this.configuration.accessToken( + "OAuth2AuthorizationCodeBearer", + [], + ); + } + const response = await this.request( { path: `/v1/transcripts/{transcript_id}`.replace( @@ -212,6 +224,14 @@ export class DefaultApi extends runtime.BaseAPI { const headerParameters: runtime.HTTPHeaders = {}; + if (this.configuration && this.configuration.accessToken) { + // oauth required + headerParameters["Authorization"] = await this.configuration.accessToken( + "OAuth2AuthorizationCodeBearer", + [], + ); + } + const response = await this.request( { path: `/v1/transcripts/{transcript_id}`.replace( @@ -299,6 +319,61 @@ export class DefaultApi extends runtime.BaseAPI { return await response.value(); } + /** + * Transcript Get Audio Mp3 + */ + async v1TranscriptGetAudioMp3Raw( + requestParameters: V1TranscriptGetAudioMp3Request, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise> { + if ( + requestParameters.transcriptId === null || + requestParameters.transcriptId === undefined + ) { + throw new runtime.RequiredError( + "transcriptId", + "Required parameter requestParameters.transcriptId was null or undefined when calling v1TranscriptGetAudioMp3.", + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + const response = await this.request( + { + path: `/v1/transcripts/{transcript_id}/audio/mp3`.replace( + `{${"transcript_id"}}`, + encodeURIComponent(String(requestParameters.transcriptId)), + ), + method: "GET", + headers: headerParameters, + query: queryParameters, + }, + initOverrides, + ); + + if (this.isJsonMime(response.headers.get("content-type"))) { + return new runtime.JSONApiResponse(response); + } else { + return new runtime.TextApiResponse(response) as any; + } + } + + /** + * Transcript Get Audio Mp3 + */ + async v1TranscriptGetAudioMp3( + requestParameters: V1TranscriptGetAudioMp3Request, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise { + const response = await this.v1TranscriptGetAudioMp3Raw( + requestParameters, + initOverrides, + ); + return await response.value(); + } + /** * Transcript Get Topics */ @@ -510,6 +585,14 @@ export class DefaultApi extends runtime.BaseAPI { headerParameters["Content-Type"] = "application/json"; + if (this.configuration && this.configuration.accessToken) { + // oauth required + headerParameters["Authorization"] = await this.configuration.accessToken( + "OAuth2AuthorizationCodeBearer", + [], + ); + } + const response = await this.request( { path: `/v1/transcripts/{transcript_id}`.replace( @@ -566,6 +649,14 @@ export class DefaultApi extends runtime.BaseAPI { headerParameters["Content-Type"] = "application/json"; + if (this.configuration && this.configuration.accessToken) { + // oauth required + headerParameters["Authorization"] = await this.configuration.accessToken( + "OAuth2AuthorizationCodeBearer", + [], + ); + } + const response = await this.request( { path: `/v1/transcripts`, @@ -615,6 +706,14 @@ export class DefaultApi extends runtime.BaseAPI { const headerParameters: runtime.HTTPHeaders = {}; + if (this.configuration && this.configuration.accessToken) { + // oauth required + headerParameters["Authorization"] = await this.configuration.accessToken( + "OAuth2AuthorizationCodeBearer", + [], + ); + } + const response = await this.request( { path: `/v1/transcripts`, @@ -643,4 +742,49 @@ export class DefaultApi extends runtime.BaseAPI { ); return await response.value(); } + + /** + * User Me + */ + async v1UserMeRaw( + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise> { + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.accessToken) { + // oauth required + headerParameters["Authorization"] = await this.configuration.accessToken( + "OAuth2AuthorizationCodeBearer", + [], + ); + } + + const response = await this.request( + { + path: `/v1/me`, + method: "GET", + headers: headerParameters, + query: queryParameters, + }, + initOverrides, + ); + + if (this.isJsonMime(response.headers.get("content-type"))) { + return new runtime.JSONApiResponse(response); + } else { + return new runtime.TextApiResponse(response) as any; + } + } + + /** + * User Me + */ + async v1UserMe( + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise { + const response = await this.v1UserMeRaw(initOverrides); + return await response.value(); + } } diff --git a/www/app/api/models/UserInfo.ts b/www/app/api/models/UserInfo.ts new file mode 100644 index 00000000..65d84951 --- /dev/null +++ b/www/app/api/models/UserInfo.ts @@ -0,0 +1,84 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * FastAPI + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from "../runtime"; +/** + * + * @export + * @interface UserInfo + */ +export interface UserInfo { + /** + * + * @type {any} + * @memberof UserInfo + */ + sub: any | null; + /** + * + * @type {any} + * @memberof UserInfo + */ + email: any | null; + /** + * + * @type {any} + * @memberof UserInfo + */ + emailVerified: any | null; +} + +/** + * Check if a given object implements the UserInfo interface. + */ +export function instanceOfUserInfo(value: object): boolean { + let isInstance = true; + isInstance = isInstance && "sub" in value; + isInstance = isInstance && "email" in value; + isInstance = isInstance && "emailVerified" in value; + + return isInstance; +} + +export function UserInfoFromJSON(json: any): UserInfo { + return UserInfoFromJSONTyped(json, false); +} + +export function UserInfoFromJSONTyped( + json: any, + ignoreDiscriminator: boolean, +): UserInfo { + if (json === undefined || json === null) { + return json; + } + return { + sub: json["sub"], + email: json["email"], + emailVerified: json["email_verified"], + }; +} + +export function UserInfoToJSON(value?: UserInfo | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + sub: value.sub, + email: value.email, + email_verified: value.emailVerified, + }; +} diff --git a/www/app/api/models/index.ts b/www/app/api/models/index.ts index 7de1ce39..d872c782 100644 --- a/www/app/api/models/index.ts +++ b/www/app/api/models/index.ts @@ -8,4 +8,5 @@ export * from "./PageGetTranscript"; export * from "./RtcOffer"; export * from "./TranscriptTopic"; export * from "./UpdateTranscript"; +export * from "./UserInfo"; export * from "./ValidationError"; diff --git a/www/app/layout.tsx b/www/app/layout.tsx index 99733717..fc7913f5 100644 --- a/www/app/layout.tsx +++ b/www/app/layout.tsx @@ -1,6 +1,8 @@ import "./styles/globals.scss"; import { Roboto } from "next/font/google"; import { Metadata } from "next"; +import FiefWrapper from "./(auth)/fiefWrapper"; +import UserInfo from "./(auth)/userInfo"; const roboto = Roboto({ subsets: ["latin"], weight: "400" }); @@ -52,7 +54,21 @@ export default function RootLayout({ children }) { return ( - {children} + +
+
+ + +
+

Reflector

+

+ Capture The Signal, Not The Noise +

+
+ {children} +
+
+
); diff --git a/www/app/lib/fief.ts b/www/app/lib/fief.ts new file mode 100644 index 00000000..a2e610b2 --- /dev/null +++ b/www/app/lib/fief.ts @@ -0,0 +1,47 @@ +"use client"; +import { Fief, FiefUserInfo } from "@fief/fief"; +import { FiefAuth, IUserInfoCache } from "@fief/fief/nextjs"; + +export const SESSION_COOKIE_NAME = "reflector-auth"; + +export const fiefClient = new Fief({ + baseURL: process.env.FIEF_URL ?? "", + clientId: process.env.FIEF_CLIENT_ID ?? "", + clientSecret: process.env.FIEF_CLIENT_SECRET ?? "", +}); + +class MemoryUserInfoCache implements IUserInfoCache { + private storage: Record; + + constructor() { + this.storage = {}; + } + + async get(id: string): Promise { + const userinfo = this.storage[id]; + if (userinfo) { + return userinfo; + } + return null; + } + + async set(id: string, userinfo: FiefUserInfo): Promise { + this.storage[id] = userinfo; + } + + async remove(id: string): Promise { + this.storage[id] = undefined; + } + + async clear(): Promise { + this.storage = {}; + } +} + +export const fiefAuth = new FiefAuth({ + client: fiefClient, + sessionCookieName: SESSION_COOKIE_NAME, + redirectURI: "http://localhost:3000/auth-callback", + logoutRedirectURI: "http://localhost:3000", + userInfoCache: new MemoryUserInfoCache(), +}); diff --git a/www/app/lib/getApi.ts b/www/app/lib/getApi.ts new file mode 100644 index 00000000..198bfd59 --- /dev/null +++ b/www/app/lib/getApi.ts @@ -0,0 +1,18 @@ +import { Configuration } from "../api/runtime"; +import { DefaultApi } from "../api/apis/DefaultApi"; + +import { useFiefAccessTokenInfo } from "@fief/fief/nextjs/react"; + +export default function getApi(): DefaultApi { + const accessTokenInfo = useFiefAccessTokenInfo(); + + const apiConfiguration = new Configuration({ + basePath: process.env.NEXT_PUBLIC_API_URL, + accessToken: accessTokenInfo + ? "Bearer " + accessTokenInfo.access_token + : undefined, + }); + const api = new DefaultApi(apiConfiguration); + + return api; +} diff --git a/www/app/lib/random.tsx b/www/app/lib/random.ts similarity index 100% rename from www/app/lib/random.tsx rename to www/app/lib/random.ts diff --git a/www/app/lib/time.tsx b/www/app/lib/time.ts similarity index 100% rename from www/app/lib/time.tsx rename to www/app/lib/time.ts diff --git a/www/app/lib/user.ts b/www/app/lib/user.ts new file mode 100644 index 00000000..26740e47 --- /dev/null +++ b/www/app/lib/user.ts @@ -0,0 +1,21 @@ +export async function getCurrentUser(): Promise { + try { + const response = await fetch("/api/current-user"); + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + const data = await response.json(); + + // Ensure the data structure is as expected + if (data.userinfo && data.access_token_info) { + return data; + } else { + throw new Error("Unexpected data structure"); + } + } catch (error) { + console.error("Error fetching the user data:", error); + throw error; // or you can return an appropriate fallback or error indicator + } +} diff --git a/www/app/transcripts/new/page.tsx b/www/app/transcripts/new/page.tsx index 80bbe9fe..949b477f 100644 --- a/www/app/transcripts/new/page.tsx +++ b/www/app/transcripts/new/page.tsx @@ -26,12 +26,7 @@ const App = () => { const webSockets = useWebSockets(transcript.response?.id); return ( -
-
-

Reflector

-

Capture The Signal, Not The Noise

-
- + <> { @@ -48,7 +43,7 @@ const App = () => { topics={webSockets.topics} disconnected={disconnected} /> -
+ ); }; diff --git a/www/app/transcripts/useTranscript.tsx b/www/app/transcripts/useTranscript.tsx index 11510020..040004ac 100644 --- a/www/app/transcripts/useTranscript.tsx +++ b/www/app/transcripts/useTranscript.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { DefaultApi, V1TranscriptsCreateRequest } from "../api/apis/DefaultApi"; import { Configuration } from "../api/runtime"; import { GetTranscript } from "../api"; +import getApi from "../lib/getApi"; type UseTranscript = { response: GetTranscript | null; @@ -15,10 +16,7 @@ const useTranscript = (): UseTranscript => { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const apiConfiguration = new Configuration({ - basePath: process.env.NEXT_PUBLIC_API_URL, - }); - const api = new DefaultApi(apiConfiguration); + const api = getApi(); const createTranscript = () => { setLoading(true); diff --git a/www/app/transcripts/useWebRTC.tsx b/www/app/transcripts/useWebRTC.tsx index ea622bb2..2f9f3c25 100644 --- a/www/app/transcripts/useWebRTC.tsx +++ b/www/app/transcripts/useWebRTC.tsx @@ -5,6 +5,7 @@ import { V1TranscriptRecordWebrtcRequest, } from "../api/apis/DefaultApi"; import { Configuration } from "../api/runtime"; +import getApi from "../lib/getApi"; const useWebRTC = ( stream: MediaStream | null, @@ -17,10 +18,7 @@ const useWebRTC = ( return; } - const apiConfiguration = new Configuration({ - basePath: process.env.NEXT_PUBLIC_API_URL, - }); - const api = new DefaultApi(apiConfiguration); + const api = getApi(); let p: Peer = new Peer({ initiator: true, stream: stream }); diff --git a/www/middleware.ts b/www/middleware.ts new file mode 100644 index 00000000..f65a3a67 --- /dev/null +++ b/www/middleware.ts @@ -0,0 +1,20 @@ +import type { NextRequest } from "next/server"; + +import { fiefAuth } from "./app/lib/fief"; + +const authMiddleware = fiefAuth.middleware([ + { + matcher: "/private", + parameters: {}, + }, + { + matcher: "/castles/:path*", + parameters: { + permissions: ["castles:read"], + }, + }, +]); + +export async function middleware(request: NextRequest) { + return authMiddleware(request); +} diff --git a/www/package.json b/www/package.json index ee03840a..2173ce5a 100644 --- a/www/package.json +++ b/www/package.json @@ -11,6 +11,7 @@ "openapi": "openapi-generator-cli generate -i http://localhost:1250/openapi.json -g typescript-fetch -o app/api && yarn format" }, "dependencies": { + "@fief/fief": "^0.13.5", "@fortawesome/fontawesome-svg-core": "^6.4.0", "@fortawesome/free-solid-svg-icons": "^6.4.0", "@fortawesome/react-fontawesome": "^0.2.0", diff --git a/www/pages/api/current-user.ts b/www/pages/api/current-user.ts new file mode 100644 index 00000000..ccb53362 --- /dev/null +++ b/www/pages/api/current-user.ts @@ -0,0 +1,3 @@ +import { fiefAuth } from "../../app/lib/fief"; + +export default fiefAuth.currentUser(); diff --git a/www/pages/forbidden.tsx b/www/pages/forbidden.tsx new file mode 100644 index 00000000..31a746fc --- /dev/null +++ b/www/pages/forbidden.tsx @@ -0,0 +1,7 @@ +import type { NextPage } from "next"; + +const Forbidden: NextPage = () => { + return

Sorry, you are not authorized to access this page.

; +}; + +export default Forbidden; diff --git a/www/yarn.lock b/www/yarn.lock index d2ae9035..dc6ec909 100644 --- a/www/yarn.lock +++ b/www/yarn.lock @@ -14,6 +14,16 @@ dependencies: regenerator-runtime "^0.14.0" +"@fief/fief@^0.13.5": + version "0.13.5" + resolved "https://registry.yarnpkg.com/@fief/fief/-/fief-0.13.5.tgz#01ac833ddff0b84f2e1737cc168721568b0e7a39" + integrity sha512-st4+/Rc1rw18B6RtqLmC6t9k0pnYLG7AqMe0JYjKYcamCNnABwl198ZJt6HShtaZKI5maHsh99UoCA3MqpbWsQ== + dependencies: + encoding "^0.1.13" + jose "^4.6.0" + node-fetch "^2.6.7" + path-to-regexp "^6.2.1" + "@fortawesome/fontawesome-common-types@6.4.0": version "6.4.0" resolved "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.0.tgz" @@ -868,6 +878,13 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +encoding@^0.1.13: + version "0.1.13" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" + integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== + dependencies: + iconv-lite "^0.6.2" + err-code@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/err-code/-/err-code-3.0.1.tgz" @@ -1108,6 +1125,13 @@ iconv-lite@^0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +iconv-lite@^0.6.2: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + ieee754@^1.1.13, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" @@ -1247,6 +1271,11 @@ jiti@^1.18.2: resolved "https://registry.npmjs.org/jiti/-/jiti-1.19.1.tgz" integrity sha512-oVhqoRDaBXf7sjkll95LHVS6Myyyb1zaunVwk4Z0+WPSW4gjS0pl01zYKHScTuyEhQsFxV5L4DR5r+YqSyqyyg== +jose@^4.6.0: + version "4.14.4" + resolved "https://registry.yarnpkg.com/jose/-/jose-4.14.4.tgz#59e09204e2670c3164ee24cbfe7115c6f8bff9ca" + integrity sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g== + "js-tokens@^3.0.0 || ^4.0.0": version "4.0.0" resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" @@ -1529,6 +1558,11 @@ path-to-regexp@3.2.0: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.2.0.tgz#fa7877ecbc495c601907562222453c43cc204a5f" integrity sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA== +path-to-regexp@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.1.tgz#d54934d6798eb9e5ef14e7af7962c945906918e5" + integrity sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw== + picocolors@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" @@ -1786,7 +1820,7 @@ safe-buffer@^5.1.0, safe-buffer@~5.2.0: resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -"safer-buffer@>= 2.1.2 < 3": +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==