Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
10 changes: 10 additions & 0 deletions src/backend/src/controllers/users.controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,4 +242,14 @@ export default class UsersController {
next(error);
}
}

static async validateSlackId(req: Request, res: Response, next: NextFunction) {
try {
const { slackId } = req.body;
const isValid = await UsersService.validateSlackId(slackId);
res.status(200).json({ isValid });
} catch (error: unknown) {
next(error);
}
}
}
19 changes: 18 additions & 1 deletion src/backend/src/integrations/slack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ export const checkBotInChannel = async (channelId: string): Promise<boolean> =>
};

/**
* Given a slack user id, prood.uces the name of the channel
* Given a slack user id, produces the name of the channel
* @param userId the id of the slack user
* @returns the name of the user (real name if no display name), undefined if cannot be found
*/
Expand Down Expand Up @@ -380,3 +380,20 @@ export const getReceiver = (): ExpressReceiver | null => {
// Export the getters for any direct usage if needed
export { getSlackClient };
export default getSlackClient;

/**
* Validates that a given Slack user id exists in the workspace
* All slack ids start with U. If you pass a valid user id to users.info, it returns ok: true; throws error otherwise.
* @param slackId the Slack user id to validate
* @returns true if the user exists, false otherwise
*/
export const validateSlackUserId = async (slackId: string): Promise<boolean> => {
const client = getSlackClient();
if (!client) return false;
try {
const res = await client.users.info({ user: slackId });
return res.ok === true;
} catch (error) {
return false;
}
};
2 changes: 2 additions & 0 deletions src/backend/src/routes/users.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,6 @@ userRouter.post(
UsersController.getManyUserTasks
);

userRouter.post('/validate-slack-id', nonEmptyString(body('slackId')), validateInputs, UsersController.validateSlackId);

export default userRouter;
10 changes: 10 additions & 0 deletions src/backend/src/services/users.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import authenticatedUserTransformer from '../transformers/auth-user.transformer.
import { getTaskQueryArgs } from '../prisma-query-args/tasks.query-args.js';
import taskTransformer from '../transformers/tasks.transformer.js';
import { validateUserIsPartOfFinanceTeamOrHead } from '../utils/reimbursement-requests.utils.js';
import { validateSlackUserId } from '../integrations/slack.js';

export default class UsersService {
/**
Expand Down Expand Up @@ -622,4 +623,13 @@ export default class UsersService {

return users.map(userWithScheduleSettingsTransformer);
}

/**
* Validates a user's slack id
* @param slackId the Slack user id to validate
* @returns true if the user exists, false otherwise
*/
static async validateSlackId(slackId: string): Promise<boolean> {
return validateSlackUserId(slackId);
}
}
26 changes: 26 additions & 0 deletions src/backend/tests/unit/users.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import {
import UsersService from '../../src/services/users.services.js';
import { NotFoundException, AccessDeniedException } from '../../src/utils/errors.utils.js';
import { RoleEnum } from 'shared';
import { vi, Mock } from 'vitest';
import * as slackIntegration from '../../src/integrations/slack.js';

vi.mock('../../src/integrations/slack.js', () => ({
validateSlackUserId: vi.fn()
}));

describe('User Tests', () => {
let orgId: string;
Expand Down Expand Up @@ -119,4 +125,24 @@ describe('User Tests', () => {
).rejects.toThrow(new AccessDeniedException('Guests and members cannot update user roles!'));
});
});

describe('Validate Slack id tests', () => {
it('returns true for a valid Slack id', async () => {
(slackIntegration.validateSlackUserId as Mock).mockResolvedValue(true);
const result = await UsersService.validateSlackId('U06D5RURPMF');
expect(result).toBe(true);
});

it('returns false for an invalid Slack id', async () => {
(slackIntegration.validateSlackUserId as Mock).mockResolvedValue(false);
const result = await UsersService.validateSlackId('BLAH');
expect(result).toBe(false);
});

it('returns false when Slack client is not configured', async () => {
(slackIntegration.validateSlackUserId as Mock).mockResolvedValue(false);
const result = await UsersService.validateSlackId('U06D5RURPMF');
expect(result).toBe(false);
});
});
});
10 changes: 10 additions & 0 deletions src/frontend/src/apis/users.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,13 @@ export const getManyUsersWithScheduleSettings = (userIds: string[]) => {
export const logUserOut = () => {
return axios.post<{ message: string }>(apiUrls.logUserOut());
};

/**
* Validates a user's slack id
*
* @param slackId the user's slack id
* @returns true if the slack id is valid, false otherwise
*/
export const validateSlackId = (slackId: string) => {
return axios.post<{ isValid: boolean }>(apiUrls.validateSlackId(), { slackId });
};
5 changes: 3 additions & 2 deletions src/frontend/src/app/AppAuthenticated.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ import SidebarLayout from '../layouts/SidebarLayout';
interface AppAuthenticatedProps {
userId: string;
userRole: Role;
completedOnboarding: boolean;
}

const AppAuthenticated: React.FC<AppAuthenticatedProps> = ({ userId, userRole }) => {
const AppAuthenticated: React.FC<AppAuthenticatedProps> = ({ userId, userRole, completedOnboarding }) => {
const { isLoading, isError, error, data: userSettingsData } = useSingleUserSettings(userId);

const {
Expand All @@ -64,7 +65,7 @@ const AppAuthenticated: React.FC<AppAuthenticatedProps> = ({ userId, userRole })

return (
<GlobalCarFilterProvider>
{userSettingsData.slackId || isGuest(userRole) ? (
{userSettingsData.slackId || (isGuest(userRole) && !completedOnboarding) ? (
<AppContextUser>
<SidebarLayout>
<Switch>
Expand Down
7 changes: 6 additions & 1 deletion src/frontend/src/app/AppPublic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ const AppPublic: React.FC = () => {
return <LoadingIndicator />;
}

return <AppAuthenticated userId={auth.user.userId} userRole={auth.user.role} />;
//get onboarding completion to pass to authenticated app for routing
const completedOnboarding = auth.user.onboardedTeamTypeIds.length > 0;

return (
<AppAuthenticated userId={auth.user.userId} userRole={auth.user.role} completedOnboarding={completedOnboarding} />
);
}

if (!auth.user && !auth.triedCurrent) {
Expand Down
13 changes: 12 additions & 1 deletion src/frontend/src/hooks/users.hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import {
logUserOut,
getManyUsersWithScheduleSettings,
getAllOrgUsers,
getAllOrgMembers
getAllOrgMembers,
validateSlackId
} from '../apis/users.api';
import {
User,
Expand Down Expand Up @@ -322,3 +323,13 @@ export const useLogUserOut = () => {
return data;
});
};

/**
* Custom react hook to determine if a user's slack id is valid
*/
export const useValidateSlackId = () => {
return useMutation<{ isValid: boolean }, Error, string>(['users', 'validate-slack-id'], async (slackId: string) => {
const { data } = await validateSlackId(slackId);
return data;
});
};
86 changes: 0 additions & 86 deletions src/frontend/src/pages/AcceptedPage/AcceptedPage.tsx

This file was deleted.

6 changes: 1 addition & 5 deletions src/frontend/src/pages/HomePage/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { routes } from '../../utils/routes';
import PNMHomePage from './PNMHomePage';
import OnboardingHomePage from './OnboardingHomePage';
import SelectSubteamPage from './SelectSubteamPage';
import AcceptedPage from '../AcceptedPage/AcceptedPage';
import HomePage from './HomePage';
import { useCurrentUser } from '../../hooks/users.hooks';
import IntroGuestHomePage from './IntroGuestHomePage';
Expand All @@ -23,12 +22,9 @@ const Home: React.FC = () => {
<Switch>
{completedOnboarding &&
!isAdmin(user.role) &&
[routes.HOME_PNM, routes.HOME_ONBOARDING, routes.HOME_ACCEPT].map((path) => (
<Redirect exact path={path} to={routes.HOME} />
))}
[routes.HOME_PNM, routes.HOME_ONBOARDING].map((path) => <Redirect exact path={path} to={routes.HOME} />)}
{onOnboarding && !completedOnboarding && <Redirect exact path={routes.HOME} to={routes.HOME_PNM} />}
<Route exact path={routes.HOME_SELECT_SUBTEAM} component={SelectSubteamPage} />
<Route exact path={routes.HOME_ACCEPT} component={AcceptedPage} />
<Route exact path={routes.HOME_ONBOARDING} component={OnboardingHomePage} />
<Route exact path={routes.HOME_PNM} component={PNMHomePage} />
<Route exact path={routes.HOME_MEMBER} component={HomePage} />
Expand Down
6 changes: 5 additions & 1 deletion src/frontend/src/pages/HomePage/OnboardingHomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { routes } from '../../utils/routes';
import { useCurrentOrganization } from '../../hooks/organizations.hooks';
import OnboardingProgressBar from '../../components/OnboardingProgressBar';
import ErrorPage from '../ErrorPage';
import { useCompleteOnboarding } from '../../hooks/team-types.hooks';

const OnboardingHomePage = () => {
const history = useHistory();
Expand Down Expand Up @@ -41,6 +42,8 @@ const OnboardingHomePage = () => {

const progress = useChecklistProgress(usersChecklists || [], checkedChecklists || []);

const { mutateAsync: completeOnboarding } = useCompleteOnboarding();

if (usersChecklistsIsError) {
return <ErrorPage error={usersChecklistsError} />;
}
Expand Down Expand Up @@ -69,7 +72,8 @@ const OnboardingHomePage = () => {
};

const handleConfirmModal = async () => {
history.push(routes.HOME_ACCEPT);
await completeOnboarding();
history.push(routes.HOME);
};

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import LoadingIndicator from '../../../components/LoadingIndicator';
import NERSuccessButton from '../../../components/NERSuccessButton';
import ReactHookTextField from '../../../components/ReactHookTextField';
import { useToast } from '../../../hooks/toasts.hooks';
import { useUpdateUserSettings } from '../../../hooks/users.hooks';
import { useUpdateUserSettings, useValidateSlackId } from '../../../hooks/users.hooks';
import ErrorPage from '../../ErrorPage';

interface SetUserPreferencesProps {
Expand All @@ -28,13 +28,20 @@ const SetUserPreferences: React.FC<SetUserPreferencesProps> = ({ userSettings })
const { handleSubmit, control } = useForm<{ slackId: string }>({
defaultValues: { slackId: userSettings.slackId }
});
const { mutateAsync: validateSlackId } = useValidateSlackId();

if (isLoading) return <LoadingIndicator />;
if (isError) return <ErrorPage message={error?.message} />;

const onSubmit = async ({ slackId }: { slackId: string }) => {
try {
const { isValid } = await validateSlackId(slackId);
if (!isValid) {
toast.error('Invalid Slack ID! Please check it and try again.');
return;
}
await mutateAsync({ ...userSettings, slackId });
// window.location.reload(); might not need this if it rerenders automatically
} catch (error: unknown) {
if (error instanceof Error) {
toast.error(error.message);
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/src/tests/app/AppAuthenticated.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const renderComponent = (path?: string, route?: string) => {
const RouterWrapper = routerWrapperBuilder({ path, route });
return render(
<RouterWrapper>
<AppAuthenticated userId={'1'} userRole={'GUEST'} />
<AppAuthenticated userId={'1'} userRole={'GUEST'} completedOnboarding={false} />
</RouterWrapper>
);
};
Expand Down
2 changes: 0 additions & 2 deletions src/frontend/src/utils/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ const CREDITS = `/credits`;
const HOME = `/home`;
const HOME_PNM = HOME + `/pnm`;
const HOME_SELECT_SUBTEAM = HOME + `/select-subteam`;
const HOME_ACCEPT = HOME + `/accept`;
const HOME_MEMBER = HOME + `/member`;
const HOME_ONBOARDING = HOME + `/onboarding`;

Expand Down Expand Up @@ -93,7 +92,6 @@ export const routes = {
HOME_PNM,
HOME_SELECT_SUBTEAM,
HOME_ONBOARDING,
HOME_ACCEPT,
HOME_MEMBER,

TEAMS,
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/src/utils/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const manyUserTasks = () => `${users()}/tasks/get-many`;
const currentUser = () => `${users()}/auth/current`;
const logUserOut = () => `${users()}/auth/log-out`;
const manyUsersWithScheduleSettings = () => `${users()}/scheduleSettings`;
const validateSlackId = () => `${users()}/validate-slack-id`;

/**************** Projects Endpoints ****************/
const projects = () => `${API_URL}/projects`;
Expand Down Expand Up @@ -532,6 +533,7 @@ export const apiUrls = {
currentUser,
logUserOut,
manyUsersWithScheduleSettings,
validateSlackId,

projects,
allProjectsGantt,
Expand Down
Loading