From afa2cb785a6f7745586cbe27b98520544a252f67 Mon Sep 17 00:00:00 2001 From: Dennis Wang Date: Thu, 1 May 2025 17:40:32 -0700 Subject: [PATCH 1/4] refactored to use amplify instead of outdated amazon-cognito-identity-js package --- website/src/auth.tsx | 416 ++++++++++-------- .../src/components/edit-word-audio/utils.ts | 36 +- website/src/graphql/authExchange.ts | 133 +++--- website/src/graphql/index.ts | 6 +- 4 files changed, 322 insertions(+), 269 deletions(-) diff --git a/website/src/auth.tsx b/website/src/auth.tsx index 3bb1b6bd4..890b25a44 100644 --- a/website/src/auth.tsx +++ b/website/src/auth.tsx @@ -1,17 +1,14 @@ import { - AuthenticationDetails, - CognitoIdToken, - CognitoUser, - CognitoUserAttribute, - CognitoUserPool, - CognitoUserSession, -} from "amazon-cognito-identity-js" + Amplify, + Auth, +} from "aws-amplify" import React, { createContext, useContext, useEffect, useState } from "react" import { navigate } from "vite-plugin-ssr/client/router" import { UserGroup } from "./graphql/dailp" type UserContextType = { - user: CognitoUser | null + // a little dissapointed aws amplify doesnt have a type for this :( + user: any operations: { createUser: (username: string, password: string) => void resetConfirmationCode: (email: string) => void @@ -25,48 +22,58 @@ type UserContextType = { const UserContext = createContext({} as UserContextType) -const userPool = new CognitoUserPool({ - UserPoolId: process.env["DAILP_USER_POOL"] ?? "", - ClientId: process.env["DAILP_USER_POOL_CLIENT"] ?? "", +//setting up amplify auth with env variables +Amplify.configure({ + Auth: { + region: process.env["DAILP_AWS_REGION"] ?? "", + userPoolId: process.env["DAILP_USER_POOL"] ?? "", + userPoolWebClientId: process.env["DAILP_USER_POOL_CLIENT"] ?? "", + identityPoolId: process.env["DAILP_IDENTITY_POOL"] ?? "", + } }) -/** Get the currently signed in user, if there is one. */ -export function getCurrentUser(): CognitoUser | null { - return userPool.getCurrentUser() +//get currently signed in user +export async function getCurrentUser() { + try { + return await Auth.currentAuthenticatedUser() + } catch { + return null + } } +//user functions export const UserProvider = (props: { children: any }) => { - const [user, setUser] = useState( - // gets the last user that logged in via this user pool - getCurrentUser() - ) + const [user, setUser] = useState(null) - function refreshToken(): Promise { - if (!user) return Promise.resolve(null) - return new Promise((res, rej) => { - // getSession() refreshes last authenticated user's tokens - user.getSession(function (err: Error, result: CognitoUserSession | null) { - if (err) rej(err) - return res(result) - }) - }) - } + // Initialize user state from any existing session + useEffect(() => { + const initUser = async () => { + try { + const currentUser = await Auth.currentAuthenticatedUser() + setUser(currentUser) + } catch (err) { + setUser(null) + } + } + initUser() + }, []) + //cases for cognito exceptions function resolveCognitoException(err: Error, userProvidedEmail?: string) { - switch (err.name) { - case CognitoErrorName.AliasExists: + const errorName = err.name + switch (errorName) { + case "AliasExistsException": alert( `An account with the email ${userProvidedEmail} already exists. Please use a different email.` ) break - case CognitoErrorName.CodeDeliveryFailure: - alert(`We could not send a confirmation code to ${ - user?.getUsername() || userProvidedEmail - }. + case "CodeDeliveryFailureException": + alert(`We could not send a confirmation code to ${user?.getUsername() || userProvidedEmail + }. Please make sure you have typed the correct email. If this issue persists, wait and try again later.`) break - case CognitoErrorName.CodeExpired: + case "CodeExpiredException": let userResponse = confirm( `This confirmation code has expired. Request a new code?` ) @@ -79,19 +86,19 @@ export const UserProvider = (props: { children: any }) => { alert("Please try again and enter your email.") } break - case CognitoErrorName.CodeMismatch: + case "CodeMismatchException": alert( `The code you entered does not match the code we sent you. Please double check your email or request a new code.` ) console.log("confirmation failed") break - case CognitoErrorName.InvalidPassword: + case "InvalidPasswordException": alert(err.message || JSON.stringify(err)) break - case CognitoErrorName.NotAuthorized: + case "NotAuthorizedException": alert(err.message || JSON.stringify(err)) break - case CognitoErrorName.PasswordResetRequired: + case "PasswordResetRequiredException": if ( confirm( `You must reset your password. Would you like to reset your password now?` @@ -100,7 +107,7 @@ export const UserProvider = (props: { children: any }) => { navigate("/auth/reset-password") } break - case CognitoErrorName.UserNotConfirmed: + case "UserNotConfirmedException": if ( confirm(`Your account must be confirmed before you can log in. Would you like to confirm now?`) @@ -108,11 +115,11 @@ export const UserProvider = (props: { children: any }) => { navigate("/auth/confirmation") } break - case CognitoErrorName.UsernameExists: + case "UsernameExistsException": alert(`An account with the email ${userProvidedEmail} already exists. Please sign up with a different email or try signing in with this email.`) break - case CognitoErrorName.UserNotFound: + case "UserNotFoundException": if ( confirm( `Account with email ${userProvidedEmail} not found. Would you like to create an account now?` @@ -133,153 +140,133 @@ export const UserProvider = (props: { children: any }) => { } } - // Allows persistence of the current user's session between browser refreshes. + // Refresh token if user exists useEffect(() => { - // if there is an authenticated user present - if (user != null) { - const promise = refreshToken().then((result) => { - if (!result) return null - const intervalLength = - result.getAccessToken().getExpiration() * 1000 - Date.now() - const handle = window.setInterval(() => refreshToken(), intervalLength) - return handle - }) - return () => { - promise.then((handle) => { - if (handle) { - window.clearInterval(handle) - } - }) + if (user) { + const checkSession = async () => { + try { + await Auth.currentSession() + } catch (err) { + console.error("Error refreshing session:", err) + } } - } - return + // Check session immediately + checkSession() + + // Set up periodic refresh + const REFRESH_INTERVAL = 15 * 60 * 1000 // 15 minutes + const intervalId = setInterval(checkSession, REFRESH_INTERVAL) + + return () => clearInterval(intervalId) + } else { + console.log("no user") + } }, [user]) - function createUser(email: string, password: string) { + //function to create user + async function createUser(email: string, password: string) { let emailLowercase = email.toLowerCase() console.log(`requesting adding user ${emailLowercase} to Cognito User Pool`) - let userAttributes = [{ Name: "email", Value: emailLowercase }].map( - (attr) => { - return new CognitoUserAttribute(attr) - } - ) - userPool.signUp( - emailLowercase, - password, - userAttributes, - [], - async (err, result) => { - if (err) { - resolveCognitoException(err, emailLowercase) - } else { - await navigate("/auth/confirmation") - } - } - ) - } - function resetConfirmationCode(email: string) { - let user = new CognitoUser({ - Username: email.toLowerCase(), - Pool: userPool, - }) + try { + const result = await Auth.signUp({ + username: emailLowercase, + password, + attributes: { + email: emailLowercase, + }, + }) - user.resendConfirmationCode((err, result) => { - if (err) { - resolveCognitoException(err, email) - } else { - console.log(result) - alert(`A new confirmation code was sent to ${user.getUsername()}`) - } - }) + console.log("Sign up success, result:", result) + await navigate("/auth/confirmation") + } catch (err: any) { + resolveCognitoException(err, emailLowercase) + } } - function confirmUser(email: string, confirmationCode: string) { - let user = new CognitoUser({ - Username: email.toLowerCase(), - Pool: userPool, - }) + //function to reset confirmation code + async function resetConfirmationCode(email: string) { + try { + const result = await Auth.resendSignUp(email.toLowerCase()) + console.log(result) + alert(`A new confirmation code was sent to ${email.toLowerCase()}`) + } catch (err: any) { + resolveCognitoException(err, email) + } + } - user.confirmRegistration(confirmationCode, false, (err, result) => { - if (err) { - console.log("confirmation failed. determining error") - resolveCognitoException(err, user.getUsername()) - } + //function to confirm user with confirmation code + async function confirmUser(email: string, confirmationCode: string) { + try { + const result = await Auth.confirmSignUp(email.toLowerCase(), confirmationCode) console.log("confirmation details: ", result) navigate("/auth/login") - }) + } catch (err: any) { + console.log("confirmation failed. determining error") + resolveCognitoException(err, email.toLowerCase()) + } } - function loginUser(username: string, password: string) { - const user = new CognitoUser({ - Username: username.toLowerCase(), - Pool: userPool, - }) - - const authDetails = new AuthenticationDetails({ - Username: username.toLowerCase(), - Password: password, - }) - - // logs in the user with the authentication details - user.authenticateUser(authDetails, { - onSuccess: (data: CognitoUserSession) => { - setUser(user) - navigate("/dashboard") - }, - onFailure: (err: Error) => { - console.log("Login failed. Result: ", err) - resolveCognitoException(err, user.getUsername()) - }, - newPasswordRequired: (data: CognitoUserSession) => { - console.log("New password required. Result: ", data) - alert("New password is required") - navigate("auth/reset-password") - }, - }) + //login function + async function loginUser(username: string, password: string) { + try { + const user = await Auth.signIn(username.toLowerCase(), password) + console.log("DENNIS LOGGED IN YAY ", user) + setUser(user) + navigate("/dashboard") + } catch (err: any) { + console.log("Login failed. Result: ", err) + resolveCognitoException(err, username.toLowerCase()) + } } - function resetPassword(username: string) { - // instantiate a new user with the given credentials to access Cognito API methods - const user = new CognitoUser({ - Username: username.toLowerCase(), - Pool: userPool, - }) - - user.forgotPassword({ - onSuccess: (data: CognitoUserSession) => { - setUser(user) // set current user, since a reset password flow was initialized - console.log("Reset password successful. Result: ", data) - alert("Reset email successfully sent") - }, - onFailure: (err: Error) => { - console.log("Reset password unsuccessful. Result: ", err) - resolveCognitoException(err, user.getUsername()) - }, - }) + //reset password function + async function resetPassword(username: string) { + try { + const result = await Auth.forgotPassword(username.toLowerCase()) + setUser(await getCurrentUser()) // set current user, since a reset password flow was initialized + console.log("Reset password successful. Result: ", result) + alert("Reset email successfully sent") + } catch (err: any) { + console.log("Reset password unsuccessful. Result: ", err) + resolveCognitoException(err, username.toLowerCase()) + } } - function changePassword(verificationCode: string, newPassword: string) { - user?.confirmPassword(verificationCode, newPassword, { - async onSuccess(data: string) { - setUser(null) // since user successfully changed password, reset current user's state - await navigate("/auth/login") - - console.log("Change password successful. Result: ", data) - alert("Password successfully changed") - }, - onFailure(err: Error) { - console.log("Change password unsuccessful. Result: ", err) - alert(err.message) - }, - }) + //function to change password + async function changePassword(verificationCode: string, newPassword: string) { + if (!user) { + console.error("No user is currently signed in") + return + } + + try { + await Auth.forgotPasswordSubmit( + user.username || user.getUsername(), + verificationCode, + newPassword + ) + + setUser(null) // since user successfully changed password, reset current user's state + await navigate("/auth/login") + + console.log("Change password successful.") + alert("Password successfully changed") + } catch (err: any) { + console.log("Change password unsuccessful. Result: ", err) + alert(err.message) + } } - function signOutUser() { - user?.signOut(() => { + //sign out + async function signOutUser() { + try { + await Auth.signOut() setUser(null) - }) + } catch (err) { + console.error("Error signing out:", err) + } } return ( @@ -288,8 +275,8 @@ export const UserProvider = (props: { children: any }) => { user, operations: { createUser, - resetConfirmationCode: resetConfirmationCode, - confirmUser: confirmUser, + resetConfirmationCode, + confirmUser, loginUser, resetPassword, changePassword, @@ -315,16 +302,53 @@ export const useCredentials = () => { throw new Error("`useUser` must be within a `UserProvider`") } - // gets the jwt token of the currently signed in user - const creds = context.user?.getSignInUserSession()?.getIdToken().getJwtToken() + const [token, setToken] = useState(null) + + useEffect(() => { + const getToken = async () => { + if (context.user) { + try { + //sing out + const session = await Auth.currentSession() + setToken(session.getIdToken().getJwtToken()) + } catch (err) { + console.error("Error getting token:", err) + setToken(null) + } + } else { + setToken(null) + } + } + + getToken() + }, [context.user]) - return creds ?? null + return token } export const useCognitoUserGroups = (): UserGroup[] => { const { user } = useContext(UserContext) - const groups: string[] = - user?.getSignInUserSession()?.getIdToken().payload["cognito:groups"] ?? [] + const [groups, setGroups] = useState([]) + + useEffect(() => { + const getGroups = async () => { + if (user) { + try { + const session = await Auth.currentSession() + const cognitoGroups = session.getIdToken().payload["cognito:groups"] || [] + setGroups(cognitoGroups) + } catch (err) { + console.error("Error getting user groups:", err) + setGroups([]) + } + } else { + setGroups([]) + } + } + + getGroups() + }, [user]) + return groups .map((g) => g.toUpperCase()) .filter((g): g is UserGroup => @@ -351,19 +375,27 @@ export function useUserRole(): UserRole { export const useUserId = () => { const { user } = useContext(UserContext) - const sub: string | null = - user?.getSignInUserSession()?.getIdToken().payload["sub"] ?? null - return sub -} + const [userId, setUserId] = useState(null) -function getUserSessionAsync( - user: CognitoUser -): Promise { - return new Promise((res, _rej) => { - user.getSession(function (_err: Error, result: CognitoUserSession | null) { - res(result) - }) - }) + useEffect(() => { + const getUserId = async () => { + if (user) { + try { + const session = await Auth.currentSession() + setUserId(session.getIdToken().payload["sub"] || null) + } catch (err) { + console.error("Error getting user ID:", err) + setUserId(null) + } + } else { + setUserId(null) + } + } + + getUserId() + }, [user]) + + return userId } /** @@ -371,12 +403,14 @@ function getUserSessionAsync( * * Note: You should use the `useCredentials` hook if you are writing a component. */ -export async function getIdToken(): Promise { - const user = getCurrentUser() - if (!user) return null - - const sess = await getUserSessionAsync(user) - return sess?.getIdToken() ?? null +export async function getIdToken() { + try { + const session = await Auth.currentSession() + return session.getIdToken() + } catch (err) { + console.error("Error getting ID token:", err) + return null + } } /** diff --git a/website/src/components/edit-word-audio/utils.ts b/website/src/components/edit-word-audio/utils.ts index a6410a279..7c6e4b009 100644 --- a/website/src/components/edit-word-audio/utils.ts +++ b/website/src/components/edit-word-audio/utils.ts @@ -1,11 +1,12 @@ import { CognitoIdentityClient } from "@aws-sdk/client-cognito-identity" import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3" import { fromCognitoIdentityPool } from "@aws-sdk/credential-provider-cognito-identity" -import { CognitoUser } from "amazon-cognito-identity-js" +//import { CognitoUser } from "amazon-cognito-identity-js" import { useMemo, useState } from "react" import { v4 } from "uuid" import { useUser } from "src/auth" import * as Dailp from "../../graphql/dailp" +import { Auth } from "aws-amplify" type UploadAudioState = "ready" | "uploading" | "error" @@ -26,8 +27,7 @@ export function useAudioUpload(wordId: string) { async function (data: Blob) { setUploadAudioState("uploading") try { - const { resourceUrl } = await uploadContributorAudioToS3(user!, data) - // const resourceUrl = "https://" + prompt("url?") + const { resourceUrl } = await uploadContributorAudioToS3(data) const result = await contributeAudio({ input: { wordId, @@ -48,7 +48,7 @@ export function useAudioUpload(wordId: string) { setUploadAudioState("ready") return true }, - [user, contributeAudio, wordId] + [contributeAudio, wordId] ) function clearError() { @@ -61,7 +61,6 @@ export function useAudioUpload(wordId: string) { } export async function uploadContributorAudioToS3( - user: CognitoUser, data: Blob ) { // Get the Amazon Cognito ID token for the user. 'getToken()' below. @@ -72,6 +71,9 @@ export async function uploadContributorAudioToS3( // let loginData = { // [COGNITO_ID]: token, // } + try{ + const session = await Auth.currentSession() + const jwtToken = session.getIdToken().getJwtToken() const s3Client = new S3Client({ region: REGION, @@ -82,19 +84,23 @@ export async function uploadContributorAudioToS3( }), logins: { [`cognito-idp.${REGION}.amazonaws.com/${process.env["DAILP_USER_POOL"]}`]: - user.getSignInUserSession()?.getIdToken().getJwtToken() ?? "", + jwtToken, }, }), }) - const key = `user-uploaded-audio/${v4()}` - await s3Client.send( - new PutObjectCommand({ - Body: data, - Bucket: BUCKET, - Key: key, - }) - ) + const key = `user-uploaded-audio/${v4()}` + await s3Client.send( + new PutObjectCommand({ + Body: data, + Bucket: BUCKET, + Key: key, + }) + ) - return { resourceUrl: `https://${process.env["CF_URL"]}/${key}` } + return { resourceUrl: `https://${process.env["CF_URL"]}/${key}` } + } catch (error) { + console.error("Error uploading to S3:", error) + throw error + } } diff --git a/website/src/graphql/authExchange.ts b/website/src/graphql/authExchange.ts index f88f60c4b..090a8bc99 100644 --- a/website/src/graphql/authExchange.ts +++ b/website/src/graphql/authExchange.ts @@ -1,70 +1,83 @@ -import { authExchange } from "@urql/exchange-auth" -import { CognitoIdToken } from "amazon-cognito-identity-js" +import { authExchange as urqlAuthExchange } from "@urql/exchange-auth" +import { Auth } from "aws-amplify" import { makeOperation } from "urql" -import { getCurrentUser, getIdToken } from "src/auth" -import { - GRAPHQL_URL_READ, - GRAPHQL_URL_WRITE, - WP_GRAPHQL_URL, -} from "src/graphql" -const secondsSinceEpoch = () => Date.now() / 1000 +// Explicitly define these constants here to avoid import issues +export const GRAPHQL_URL_READ = `${process.env["DAILP_API_URL"] ?? ""}/graphql` +export const GRAPHQL_URL_WRITE = `${process.env["DAILP_API_URL"] ?? ""}/graphql-edit` +export const WP_GRAPHQL_URL = "https://wp.dailp.northeastern.edu/graphql" -const isExpired = (token: CognitoIdToken) => - token.getExpiration() <= secondsSinceEpoch() +const secondsSinceEpoch = () => Math.floor(Date.now() / 1000) -export const cognitoAuthExchange = () => - authExchange<{ token: CognitoIdToken }>({ - async getAuth({ authState }) { - if (!authState || isExpired(authState.token)) { - const token = await getIdToken() - if (token) return { token } - } - - return null - }, +const isExpired = (token: string) => { + const parts = token.split(".") + if (parts.length !== 3) return false + + const payload = parts[1] + if (!payload) return false + + try { + const decoded = JSON.parse(atob(payload)) + return secondsSinceEpoch() > (decoded.exp || 0) + } catch { + return false + } +} - willAuthError({ authState }) { - if (getCurrentUser()) { - // if we are signed in, we must have a valid token - return !authState || isExpired(authState.token) - } else if (authState) { - // we aren't signed in but we have a token, we need to get rid of it - return true - } else { - return false - } - }, - - addAuthToOperation({ authState, operation }) { - // We don't send tokens if we don't have them or if we are talking to Wordpress - if (!authState || operation.context.url === WP_GRAPHQL_URL) { - return operation +export const authExchange = urqlAuthExchange<{ token: string }>({ + async getAuth() { + try { + const session = await Auth.currentSession() + const jwtToken = session.getIdToken().getJwtToken() + if (typeof jwtToken === 'string') { + return { token: jwtToken } } + return null + } catch { + return null + } + }, + addAuthToOperation({ authState, operation }) { + // Don't send tokens if we don't have them or if we are talking to Wordpress + if (!authState || !authState.token || operation.context.url === WP_GRAPHQL_URL) { + return operation + } - const fetchOptions = - typeof operation.context.fetchOptions === "function" - ? operation.context.fetchOptions() - : operation.context.fetchOptions || {} + const fetchOptions = + typeof operation.context.fetchOptions === "function" + ? operation.context.fetchOptions() + : operation.context.fetchOptions || {} - // we need to send requests with JWTs to the /graphql-edit endpoint, which - // has the appropriate authorizer to accept them - const url = - operation.context.url === GRAPHQL_URL_READ - ? GRAPHQL_URL_WRITE - : operation.context.url + // we need to send requests with JWTs to the /graphql-edit endpoint, which + // has the appropriate authorizer to accept them + const url = + operation.context.url === GRAPHQL_URL_READ + ? GRAPHQL_URL_WRITE + : operation.context.url - // get the authentication token to the headers - return makeOperation(operation.kind, operation, { - ...operation.context, - url, - fetchOptions: { - ...fetchOptions, - headers: { - ...fetchOptions.headers, - Authorization: `Bearer ${authState.token.getJwtToken()}`, - }, + return makeOperation(operation.kind, operation, { + ...operation.context, + url, + fetchOptions: { + ...fetchOptions, + headers: { + ...fetchOptions.headers, + Authorization: `Bearer ${authState.token}`, }, - }) - }, - }) + }, + }) + }, + willAuthError({ authState }) { + if (!authState || !authState.token) { + return true + } + + // Try to refresh the session when checking for auth errors + // This doesn't block, but helps keep the token fresh + Auth.currentSession().catch(e => { + console.error("Failed to refresh authentication", e) + }) + + return false + }, +}) diff --git a/website/src/graphql/index.ts b/website/src/graphql/index.ts index e6dc8ce17..fae74047b 100644 --- a/website/src/graphql/index.ts +++ b/website/src/graphql/index.ts @@ -10,7 +10,7 @@ import { devtoolsExchange } from "@urql/devtools" import { cacheExchange } from "@urql/exchange-graphcache" import fetch from "isomorphic-unfetch" import { Environment, deploymentEnvironment } from "../env" -import { cognitoAuthExchange } from "./authExchange" +import { authExchange } from "./authExchange" export const GRAPHQL_URL_READ = process.env["DAILP_API_URL"] + "/graphql" export const GRAPHQL_URL_WRITE = process.env["DAILP_API_URL"] + "/graphql-edit" @@ -86,7 +86,7 @@ const maybeDevExchange = * - devtoolsExchange (not added in production) * - sharedCache * - Any caller-specified exchanges (see `exchanges` param) - * - cognitoAuthExchange (client-side only) + * - authExchange (client-side only) * - fetchExchange * @param serverSide - if this client is for server-side use. Otherwise client-side is assumed. * @param exchanges - extra exchanges the caller needs (ie. a specific client/server SSR exchange) @@ -100,7 +100,7 @@ const customClient = (serverSide: boolean, exchanges: Exchange[]) => sharedCache, ...exchanges, // only the client needs the auth link - ...(serverSide ? [] : [cognitoAuthExchange()]), + ...(serverSide ? [] : [authExchange]), fetchExchange, ], suspense: serverSide, From 34a6b8f837eb9381fd5bf0184cec199acd6635dc Mon Sep 17 00:00:00 2001 From: Dennis Wang Date: Fri, 2 May 2025 21:10:51 -0700 Subject: [PATCH 2/4] removed all outdated cognito library usage --- website/src/components/edit-word-audio/utils.ts | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/website/src/components/edit-word-audio/utils.ts b/website/src/components/edit-word-audio/utils.ts index 7c6e4b009..ada490d08 100644 --- a/website/src/components/edit-word-audio/utils.ts +++ b/website/src/components/edit-word-audio/utils.ts @@ -1,7 +1,4 @@ -import { CognitoIdentityClient } from "@aws-sdk/client-cognito-identity" import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3" -import { fromCognitoIdentityPool } from "@aws-sdk/credential-provider-cognito-identity" -//import { CognitoUser } from "amazon-cognito-identity-js" import { useMemo, useState } from "react" import { v4 } from "uuid" import { useUser } from "src/auth" @@ -72,21 +69,11 @@ export async function uploadContributorAudioToS3( // [COGNITO_ID]: token, // } try{ - const session = await Auth.currentSession() - const jwtToken = session.getIdToken().getJwtToken() + const credentials = await Auth.currentCredentials() const s3Client = new S3Client({ region: REGION, - credentials: fromCognitoIdentityPool({ - identityPoolId: process.env["DAILP_IDENTITY_POOL"]!, - client: new CognitoIdentityClient({ - region: REGION, - }), - logins: { - [`cognito-idp.${REGION}.amazonaws.com/${process.env["DAILP_USER_POOL"]}`]: - jwtToken, - }, - }), + credentials: Auth.essentialCredentials(credentials) }) const key = `user-uploaded-audio/${v4()}` From c4cf66dcf9bcede64cb5bab837f7ecf061983628 Mon Sep 17 00:00:00 2001 From: Dennis Wang Date: Fri, 2 May 2025 21:16:13 -0700 Subject: [PATCH 3/4] packages --- website/package.json | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/website/package.json b/website/package.json index 94ff980ed..a4fbb5a3b 100644 --- a/website/package.json +++ b/website/package.json @@ -19,6 +19,11 @@ "license": "MIT", "private": true, "dependencies": { + "@aws-amplify/auth": "^5.6.6", + "@aws-sdk/client-cognito-identity": "^3.279.0", + "@aws-sdk/client-s3": "^3.279.0", + "@aws-sdk/credential-provider-cognito-identity": "^3.279.0", + "@headlessui/react": "^1.6.2", "@fontsource/charis-sil": "^4.5.7", "@fontsource/quattrocento-sans": "^4.5.5", "@reach/dialog": "^0.16.2", @@ -26,10 +31,10 @@ "@react-hook/debounce": "^4.0.0", "@urql/core": "^4.1.1", "@urql/devtools": "^2.0.3", - "@urql/exchange-auth": "^1", - "@urql/exchange-graphcache": "^6.3.1", - "amazon-cognito-identity-js": "^5.2.9", - "aws-amplify": "^4.3.20", + "@urql/exchange-auth": "^2.1.5", + "@urql/exchange-graphcache": "^5.0.8", + "@urql/exchange-multipart-fetch": "^1.0.1", + "aws-amplify": "^5.3.13", "classnames": "^2.3.1", "graphql": "^16.3.0", "graphql-tag": "^2.11.0", From e5056656411ec544590b274e275c31948e7e2763 Mon Sep 17 00:00:00 2001 From: Dennis Wang <66754085+denniwang@users.noreply.github.com> Date: Thu, 21 Aug 2025 12:52:43 -0400 Subject: [PATCH 4/4] dev-check --- website/src/auth.tsx | 27 ++++++++++--------- .../src/components/edit-word-audio/utils.ts | 18 ++++++------- website/src/graphql/authExchange.ts | 22 +++++++++------ 3 files changed, 37 insertions(+), 30 deletions(-) diff --git a/website/src/auth.tsx b/website/src/auth.tsx index 064399bec..9b1693a1c 100644 --- a/website/src/auth.tsx +++ b/website/src/auth.tsx @@ -1,13 +1,10 @@ -import { - Amplify, - Auth, -} from "aws-amplify" +import { Amplify, Auth } from "aws-amplify" import React, { createContext, useContext, useEffect, useState } from "react" import { navigate } from "vite-plugin-ssr/client/router" import { UserGroup } from "./graphql/dailp" type UserContextType = { - // a little dissapointed aws amplify doesnt have a type for this :( + // a little dissapointed aws amplify doesnt have a type for this :( user: any operations: { createUser: (username: string, password: string) => void @@ -29,7 +26,7 @@ Amplify.configure({ userPoolId: process.env["DAILP_USER_POOL"] ?? "", userPoolWebClientId: process.env["DAILP_USER_POOL_CLIENT"] ?? "", identityPoolId: process.env["DAILP_IDENTITY_POOL"] ?? "", - } + }, }) //get currently signed in user @@ -41,7 +38,7 @@ export async function getCurrentUser() { } } -//user functions +//user functions export const UserProvider = (props: { children: any }) => { const [user, setUser] = useState(null) @@ -68,8 +65,9 @@ export const UserProvider = (props: { children: any }) => { ) break case "CodeDeliveryFailureException": - alert(`We could not send a confirmation code to ${user?.getUsername() || userProvidedEmail - }. + alert(`We could not send a confirmation code to ${ + user?.getUsername() || userProvidedEmail + }. Please make sure you have typed the correct email. If this issue persists, wait and try again later.`) break @@ -161,6 +159,7 @@ export const UserProvider = (props: { children: any }) => { return () => clearInterval(intervalId) } else { console.log("no user") + return undefined } }, [user]) @@ -199,7 +198,10 @@ export const UserProvider = (props: { children: any }) => { //function to confirm user with confirmation code async function confirmUser(email: string, confirmationCode: string) { try { - const result = await Auth.confirmSignUp(email.toLowerCase(), confirmationCode) + const result = await Auth.confirmSignUp( + email.toLowerCase(), + confirmationCode + ) console.log("confirmation details: ", result) navigate("/auth/login") } catch (err: any) { @@ -221,7 +223,7 @@ export const UserProvider = (props: { children: any }) => { } } - //reset password function + //reset password function async function resetPassword(username: string) { try { const result = await Auth.forgotPassword(username.toLowerCase()) @@ -335,7 +337,8 @@ export const useCognitoUserGroups = (): UserGroup[] => { if (user) { try { const session = await Auth.currentSession() - const cognitoGroups = session.getIdToken().payload["cognito:groups"] || [] + const cognitoGroups = + session.getIdToken().payload["cognito:groups"] || [] setGroups(cognitoGroups) } catch (err) { console.error("Error getting user groups:", err) diff --git a/website/src/components/edit-word-audio/utils.ts b/website/src/components/edit-word-audio/utils.ts index ada490d08..71ada1efe 100644 --- a/website/src/components/edit-word-audio/utils.ts +++ b/website/src/components/edit-word-audio/utils.ts @@ -1,9 +1,9 @@ import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3" +import { Auth } from "aws-amplify" import { useMemo, useState } from "react" import { v4 } from "uuid" import { useUser } from "src/auth" import * as Dailp from "../../graphql/dailp" -import { Auth } from "aws-amplify" type UploadAudioState = "ready" | "uploading" | "error" @@ -57,9 +57,7 @@ export function useAudioUpload(wordId: string) { return [uploadAudio, uploadAudioState, clearError] as const } -export async function uploadContributorAudioToS3( - data: Blob -) { +export async function uploadContributorAudioToS3(data: Blob) { // Get the Amazon Cognito ID token for the user. 'getToken()' below. const REGION = process.env["DAILP_AWS_REGION"] // TF Stage matches infra environment names: "dev" "prod" or "uat". If TF_STAGE not found, fall back to dev @@ -68,13 +66,13 @@ export async function uploadContributorAudioToS3( // let loginData = { // [COGNITO_ID]: token, // } - try{ - const credentials = await Auth.currentCredentials() + try { + const credentials = await Auth.currentCredentials() - const s3Client = new S3Client({ - region: REGION, - credentials: Auth.essentialCredentials(credentials) - }) + const s3Client = new S3Client({ + region: REGION, + credentials: Auth.essentialCredentials(credentials), + }) const key = `user-uploaded-audio/${v4()}` await s3Client.send( diff --git a/website/src/graphql/authExchange.ts b/website/src/graphql/authExchange.ts index 090a8bc99..f8463f8c5 100644 --- a/website/src/graphql/authExchange.ts +++ b/website/src/graphql/authExchange.ts @@ -4,7 +4,9 @@ import { makeOperation } from "urql" // Explicitly define these constants here to avoid import issues export const GRAPHQL_URL_READ = `${process.env["DAILP_API_URL"] ?? ""}/graphql` -export const GRAPHQL_URL_WRITE = `${process.env["DAILP_API_URL"] ?? ""}/graphql-edit` +export const GRAPHQL_URL_WRITE = `${ + process.env["DAILP_API_URL"] ?? "" +}/graphql-edit` export const WP_GRAPHQL_URL = "https://wp.dailp.northeastern.edu/graphql" const secondsSinceEpoch = () => Math.floor(Date.now() / 1000) @@ -12,10 +14,10 @@ const secondsSinceEpoch = () => Math.floor(Date.now() / 1000) const isExpired = (token: string) => { const parts = token.split(".") if (parts.length !== 3) return false - + const payload = parts[1] if (!payload) return false - + try { const decoded = JSON.parse(atob(payload)) return secondsSinceEpoch() > (decoded.exp || 0) @@ -29,7 +31,7 @@ export const authExchange = urqlAuthExchange<{ token: string }>({ try { const session = await Auth.currentSession() const jwtToken = session.getIdToken().getJwtToken() - if (typeof jwtToken === 'string') { + if (typeof jwtToken === "string") { return { token: jwtToken } } return null @@ -39,7 +41,11 @@ export const authExchange = urqlAuthExchange<{ token: string }>({ }, addAuthToOperation({ authState, operation }) { // Don't send tokens if we don't have them or if we are talking to Wordpress - if (!authState || !authState.token || operation.context.url === WP_GRAPHQL_URL) { + if ( + !authState || + !authState.token || + operation.context.url === WP_GRAPHQL_URL + ) { return operation } @@ -71,13 +77,13 @@ export const authExchange = urqlAuthExchange<{ token: string }>({ if (!authState || !authState.token) { return true } - + // Try to refresh the session when checking for auth errors // This doesn't block, but helps keep the token fresh - Auth.currentSession().catch(e => { + Auth.currentSession().catch((e) => { console.error("Failed to refresh authentication", e) }) - + return false }, })