Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UserProfile #17

Merged
merged 20 commits into from
Jan 30, 2024
Merged
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
97 changes: 79 additions & 18 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,8 @@ app.post('/api/verify-token', (req, res) => {
const token = req.headers.authorization.split("Bearer ")[1];

const decodedToken = jwt.decode(token, { complete: true });
if (!decodedToken) {
return res.status(400).json({ message: 'Invalid token format' });
}
if (!decodedToken) return res.status(400).json({ message: 'Invalid token format' });


const kid = decodedToken.header.kid;
getPublicKeyFromKeycloak(kid)
Expand Down Expand Up @@ -147,27 +146,90 @@ async function getTeamOverviewTeams(token, teams) {

app.get('/api/userProfile/:principalName', tokenVerificationMiddleware, async (req, res, next) => {
const token = req.token;
const principalName = req.user.email;
const allteamsUrl = `${DAPLA_TEAM_API_URL}/teams`;
const principalName = req.params.principalName;
const userProfileUrl = `${DAPLA_TEAM_API_URL}/users/${principalName}`;

try {
const [userProfile] = await Promise.all([
fetchAPIData(token, userProfileUrl, 'Failed to fetch userProfile')
])

res.json(userProfile);
} catch (error) {
next(error)
}
});

async function getUserProfileTeamData(token, principalName, teams) {
const teamPromises = teams._embedded.teams.map(async (team) => {
const teamUniformName = team.uniform_name;
const teamInfoUrl = `${DAPLA_TEAM_API_URL}/teams/${teamUniformName}`;
const teamGroupsUrl = `${DAPLA_TEAM_API_URL}/teams/${teamUniformName}/groups`;
const teamManagerUrl = `${DAPLA_TEAM_API_URL}/groups/${teamUniformName}-managers/users`;

const [teamInfo, teamGroups, teamManager] = await Promise.all([
fetchAPIData(token, teamInfoUrl, 'Failed to fetch team info').catch(() => {
return {
uniform_name: teamUniformName,
section_name: "Mangler seksjon"
}
}),
fetchAPIData(token, teamGroupsUrl, 'Failed to fetch groups').then(response => {
const groupPromises = response._embedded.groups.map(group => fetchUserGroups(group, token, principalName));
return Promise.all(groupPromises).then(groupsArrays => groupsArrays.flat());
}),
fetchAPIData(token, teamManagerUrl, 'Failed to fetch team manager')
]);

team['section_name'] = teamInfo.section_name;
team["manager"] = teamManager.count > 0 ? teamManager._embedded.users[0] : {
"display_name": "Mangler ansvarlig",
"principal_name": "Mangler ansvarlig",
};
team["groups"] = teamGroups;

return { ...team };
});

const resolvedTeams = await Promise.all(teamPromises);
const validTeams = resolvedTeams.filter(team => team !== null);

teams._embedded.teams = validTeams;
teams.count = validTeams.length;
return teams;
}

async function fetchUserGroups(group, token, principalName) {
const groupUsersUrl = `${DAPLA_TEAM_API_URL}/groups/${group.uniform_name}/users`;
try {
const groupUsers = await fetchAPIData(token, groupUsersUrl, 'Failed to fetch group users');
if (!groupUsers._embedded || !groupUsers._embedded.users || groupUsers._embedded.users.length === 0) {
return [];
}

return groupUsers._embedded.users
.filter(user => user.principal_name === principalName)
.map(() => group.uniform_name);
} catch (error) {
console.error(`Error processing group ${group.uniform_name}:`, error);
throw error;
}
}

app.get('/api/userProfile/:principalName/team', tokenVerificationMiddleware, async (req, res, next) => {
const token = req.token;
const principalName = req.params.principalName;
const myTeamsUrl = `${DAPLA_TEAM_API_URL}/users/${principalName}/teams`;

try {
const [allTeams, myTeams] = await Promise.all([
fetchAPIData(token, allteamsUrl, 'Failed to fetch all teams')
.then(teams => getTeamOverviewTeams(token, teams)),
const [myTeams] = await Promise.all([
fetchAPIData(token, myTeamsUrl, 'Failed to fetch my teams')
.then(teams => getTeamOverviewTeams(token, teams))
.then(teams => getUserProfileTeamData(token, principalName, teams))
])

const result = {
allTeams: {
count: allTeams.count,
...allTeams._embedded
},
myTeams: {
count: myTeams.count,
...myTeams._embedded
}
count: myTeams.count,
...myTeams._embedded
};

res.json(result);
Expand All @@ -176,7 +238,6 @@ app.get('/api/userProfile/:principalName', tokenVerificationMiddleware, async (r
}
});


async function fetchAPIData(token, url, fallbackErrorMessage) {
const response = await fetch(url, getFetchOptions(token));
const wwwAuthenticate = response.headers.get('www-authenticate');
Expand Down
1 change: 1 addition & 0 deletions src/@types/team.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export interface Team {
section_code: number
team_user_count: number
manager: User
groups?: string[]
}
4 changes: 4 additions & 0 deletions src/@types/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ export interface User {
first_name: string
last_name: string
email: string
division_name?: string
division_code?: number
section_name?: string
section_code?: number
manager?: User
photo?: string
}
2 changes: 1 addition & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import ProtectedRoute from './components/ProtectedRoute';

import Login from './pages/Login/Login';
import TeamOverview from './pages/TeamOverview/TeamOverview';
import UserProfile from './pages/UserProfile';
import UserProfile from './pages/UserProfile/UserProfile';

import { Routes, Route } from 'react-router-dom';
import { jwtRegex } from './utils/regex';
Expand Down
1 change: 1 addition & 0 deletions src/api/teamOverview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface TeamOverviewResult {
count: number
}


export const getTeamOverview = async (): Promise<TeamOverviewData | ErrorResponse> => {
const accessToken = localStorage.getItem('access_token');

Expand Down
42 changes: 36 additions & 6 deletions src/api/userProfile.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import { User } from "../@types/user";
import { Team } from "../@types/team";
import { ErrorResponse } from "../@types/error";

export interface UserProfileTeamData {
[key: string]: UserProfileTeamResult
}

export interface UserProfileTeamResult {
teams: Team[]
count: number
}


export const getUserProfile = async (principalName: string, token?: string): Promise<User | ErrorResponse> => {
const accessToken = localStorage.getItem('access_token');
principalName = principalName.replace(/@ssb\.no$/, '') + '@ssb.no';

// TODO: should not need this logic. Should be able to use principalName as is
if (principalName.endsWith('@ssb.no')) {
principalName = principalName.replace('@ssb.no', '');
}

return fetch(`/api/userProfile/${principalName}@ssb.no`, {
return fetch(`/api/userProfile/${principalName}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Expand All @@ -28,6 +35,29 @@ export const getUserProfile = async (principalName: string, token?: string): Pro
});
};

export const getUserTeamsWithGroups = async (principalName: string): Promise<UserProfileTeamResult | ErrorResponse> => {
const accessToken = localStorage.getItem('access_token');
principalName = principalName.replace(/@ssb\.no$/, '') + '@ssb.no';

return fetch(`/api/userProfile/${principalName}/team`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
}
}).then(response => {
if (!response.ok) {
console.error('Request failed with status:', response.status);
throw new Error('Request failed');
}
return response.json();
}).then(data => data as UserProfileTeamResult)
.catch(error => {
console.error('Error during fetching userProfile:', error);
throw error;
});
};

export const getUserProfileFallback = (accessToken: string): User => {
const jwt = JSON.parse(atob(accessToken.split('.')[1]));
return {
Expand Down
2 changes: 1 addition & 1 deletion src/components/PageLayout/PageLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Title, LeadParagraph } from '@statisticsnorway/ssb-component-library'

interface PageLayoutProps {
title: string,
description?: string,
description?: JSX.Element,
button?: JSX.Element,
content?: JSX.Element
}
Expand Down
1 change: 0 additions & 1 deletion src/pages/TeamOverview/TeamOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ export default function TeamOverview() {

function renderContent() {
if (error) return renderErrorAlert();

if (loading) return renderSkeletonOnLoad();

if (teamOverviewTableData) {
Expand Down
156 changes: 156 additions & 0 deletions src/pages/UserProfile/UserProfile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import pageLayoutStyles from '../../components/PageLayout/pagelayout.module.scss'
import styles from './userprofile.module.scss';

import { Dialog, Title, Text, Link } from '@statisticsnorway/ssb-component-library';
import { Skeleton } from '@mui/material';

import Table, { TableData } from '../../components/Table/Table';
import PageLayout from "../../components/PageLayout/PageLayout"

import { useContext, useEffect, useState } from "react"
import { DaplaCtrlContext } from "../../provider/DaplaCtrlProvider";
import { getGroupType } from "../../utils/utils";

import { getUserProfile, getUserTeamsWithGroups, UserProfileTeamResult } from '../../api/userProfile';

import { User } from "../../@types/user";
import { Team } from "../../@types/team";

import { useLocation, useParams } from "react-router-dom";
import { ErrorResponse } from "../../@types/error";

export default function UserProfile() {
const { setData } = useContext(DaplaCtrlContext);
const [error, setError] = useState<ErrorResponse | undefined>();
const [loadingUserProfileData, setLoadingUserProfileData] = useState<boolean>(true);
const [loadingTeamData, setLoadingTeamDataInfo] = useState<boolean>(true);
const [userProfileData, setUserProfileData] = useState<User>();
const [teamUserProfileTableData, setUserProfileTableData] = useState<TableData['data']>();
const { principalName } = useParams();
const location = useLocation();

useEffect(() => {
getUserProfile(principalName as string).then(response => {
if ((response as ErrorResponse).error) {
setError(response as ErrorResponse);
} else {
setUserProfileData(response as User);
}
}).finally(() => setLoadingUserProfileData(false))
.catch((error) => {
setError({ error: { message: error.message, code: "500" } });
})
}, [location, principalName]);

useEffect(() => {
if (userProfileData) {
getUserTeamsWithGroups(principalName as string).then(response => {
if ((response as ErrorResponse).error) {
setError(response as ErrorResponse);
} else {
setUserProfileTableData(prepTeamData(response as UserProfileTeamResult));
}
}).finally(() => setLoadingTeamDataInfo(false))
.catch((error) => {
setError({ error: { message: error.message, code: "500" } });
})
}
}, [userProfileData])

// required for breadcrumb
useEffect(() => {
if (userProfileData) {
const displayName = userProfileData.display_name.split(', ').reverse().join(' ');
userProfileData.display_name = displayName;
setData({ "displayName": displayName });
}
}, [userProfileData]);

const prepTeamData = (response: UserProfileTeamResult): TableData['data'] => {
return response.teams.map(team => ({
id: team.uniform_name,
'navn': renderTeamNameColumn(team),
'gruppe': team.groups?.map(group => getGroupType(group)).join(', '),
'epost': userProfileData?.principal_name,
'ansvarlig': team.manager.display_name.split(", ").reverse().join(" ")
}));
}

function renderTeamNameColumn(team: Team) {
return (
<>
<span>
<Link href={`/${team.uniform_name}`}>
<b>{team.uniform_name}</b>
</Link>
</span>
{team.section_name && <Text>{team.section_name}</Text>}
</>
)
}

function renderErrorAlert() {
return (
<Dialog type='warning' title="Could not fetch data">
{error?.error.message}
</Dialog >
)
}

function renderSkeletonOnLoad() {
return (
<>
<Skeleton variant="text" animation="wave" sx={{ fontSize: '5.5rem' }} width={150} />
<Skeleton variant="rectangular" animation="wave" height={200} />
</>
)
}

function renderContent() {
if (error) return renderErrorAlert();
// TODO: cheesy method to exclude showing skeleton for profile information (username etc..)
if (loadingTeamData && !loadingUserProfileData) return renderSkeletonOnLoad();

if (teamUserProfileTableData) {
const teamOverviewTableHeaderColumns = [{
id: 'navn',
label: 'Navn',
},
{
id: 'gruppe',
label: 'Gruppe',
}, {
id: 'epost',
label: 'Epost ?'
},
{
id: 'ansvarlig',
label: 'Ansvarlig'
}];
return (
<>
<Title size={2} className={pageLayoutStyles.tableTitle}>Team</Title>
<Table
columns={teamOverviewTableHeaderColumns}
data={teamUserProfileTableData as TableData['data']}
/>
</>
)
}
}

return (
<PageLayout
title={userProfileData?.display_name as string}
content={renderContent()}
description={
<>
<div className={styles.userProfileDescription}>
<Text medium>{userProfileData?.section_name}</Text>
<Text medium>{userProfileData?.principal_name}</Text>
</div>
</>
}
/>
)
}
Loading