Skip to content

Commit

Permalink
Feat: users can now select a different domain (#1050)
Browse files Browse the repository at this point in the history
  • Loading branch information
emielvanseveren authored Jun 22, 2024
1 parent 2031365 commit 5afa842
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 17 deletions.
13 changes: 13 additions & 0 deletions packages/web-main/src/components/Navbar/UserDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import {
AiOutlineLogout as LogoutIcon,
AiOutlineDown as ArrowDownIcon,
} from 'react-icons/ai';
import { IoSwapHorizontal as SwitchDomainIcon } from 'react-icons/io5';
import { useNavigate } from '@tanstack/react-router';
import { useQuery } from '@tanstack/react-query';
import { userMeQueryOptions } from 'queries/user';

const User = styled.div`
display: grid;
Expand Down Expand Up @@ -57,6 +60,10 @@ export const UserDropdown = () => {
const { logOut } = useAuth();
const navigate = useNavigate();

const { data, isPending } = useQuery(userMeQueryOptions());

const hasMultipleDomains = isPending === false && data && data.domains && data.domains.length > 1 ? true : false;

// TODO: this should be a fallback component, to stil try to logout.
if (session === null) return <div>could not get session</div>;

Expand All @@ -79,6 +86,12 @@ export const UserDropdown = () => {
label="Profile"
icon={<ProfileIcon />}
/>
<Dropdown.Menu.Item
onClick={() => navigate({ to: '/domain/select' })}
label="Switch domain"
disabled={!hasMultipleDomains}
icon={<SwitchDomainIcon />}
/>
<Dropdown.Menu.Item onClick={async () => await logOut()} label="Logout" icon={<LogoutIcon />} />
</Dropdown.Menu>
</Dropdown>
Expand Down
14 changes: 13 additions & 1 deletion packages/web-main/src/components/Navbar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FC, cloneElement, ReactElement } from 'react';
import { Link, LinkProps } from '@tanstack/react-router';
import { RequiredPermissions, Tooltip } from '@takaro/lib-components';
import { Chip, RequiredPermissions, Tooltip, useTheme } from '@takaro/lib-components';
import { UserDropdown } from './UserDropdown';
import { Nav, IconNav, Container, IconNavContainer } from './style';
import { PERMISSIONS } from '@takaro/apiclient';
Expand All @@ -24,6 +24,7 @@ import { FaDiscord as DiscordIcon } from 'react-icons/fa';
import { PermissionsGuard } from 'components/PermissionsGuard';
import { useHasPermission } from 'hooks/useHasPermission';
import { GameServerNav } from './GameServerNav';
import { TAKARO_DOMAIN_COOKIE_REGEX } from 'routes/_auth/domain.select';

const domainLinks: NavbarLink[] = [
{
Expand Down Expand Up @@ -129,6 +130,7 @@ interface NavbarProps {

export const Navbar: FC<NavbarProps> = ({ showGameServerNav }) => {
const { hasPermission } = useHasPermission([PERMISSIONS.ReadGameservers]);
const theme = useTheme();

return (
<Container animate={{ width: 325 }} transition={{ duration: 1, type: 'spring', bounce: 0.5 }}>
Expand All @@ -141,6 +143,16 @@ export const Navbar: FC<NavbarProps> = ({ showGameServerNav }) => {
</IconNavContainer>
<div style={{ width: '100%' }}>
<UserDropdown />
<div style={{ display: 'flex', justifyContent: 'center', marginTop: theme.spacing['1'], alignItems: 'center' }}>
<span style={{ marginRight: theme.spacing['0_5'] }}>Domain: </span>
<Chip
showIcon="hover"
color="secondary"
variant="outline"
label={`${document.cookie.replace(TAKARO_DOMAIN_COOKIE_REGEX, '$1')}`}
/>
</div>

<IconNav>
<Tooltip>
<Tooltip.Trigger asChild>
Expand Down
2 changes: 1 addition & 1 deletion packages/web-main/src/hooks/useAuth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { DateTime } from 'luxon';
import { getApiClient } from 'util/getApiClient';
import { redirect } from '@tanstack/react-router';

const SESSION_EXPIRES_AFTER_MINUTES = 20;
const SESSION_EXPIRES_AFTER_MINUTES = 5;

interface ExpirableSession {
session: UserOutputWithRolesDTO;
Expand Down
26 changes: 26 additions & 0 deletions packages/web-main/src/queries/user.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getApiClient } from 'util/getApiClient';
import {
APIOutput,
IdUuidDTO,
MeOutoutDTO,
UserOutputArrayDTOAPI,
UserOutputWithRolesDTO,
UserSearchInputDTO,
Expand All @@ -15,6 +16,7 @@ export const userKeys = {
all: ['users'] as const,
list: () => [...userKeys.all, 'list'] as const,
detail: (id: string) => [...userKeys.all, 'detail', id] as const,
me: () => [...userKeys.all, 'me'] as const,
};

interface RoleInput {
Expand All @@ -34,6 +36,12 @@ export const userQueryOptions = (userId: string) =>
queryFn: async () => (await getApiClient().user.userControllerGetOne(userId)).data.data,
});

export const userMeQueryOptions = () =>
queryOptions<MeOutoutDTO, AxiosError<MeOutoutDTO>>({
queryKey: userKeys.me(),
queryFn: async () => (await getApiClient().user.userControllerMe()).data.data,
});

interface IUserRoleAssign {
userId: string;
roleId: string;
Expand All @@ -57,6 +65,24 @@ export const useUserAssignRole = ({ userId }: { userId: string }) => {
);
};

interface IUserSetSelectedDomain {
domainId: string;
}
export const useUserSetSelectedDomain = () => {
const apiClient = getApiClient();
const queryClient = useQueryClient();

return mutationWrapper<void, IUserSetSelectedDomain>(
useMutation<void, AxiosError<APIOutput>, IUserSetSelectedDomain>({
mutationFn: async ({ domainId }) => (await apiClient.user.userControllerSetSelectedDomain(domainId)).data,
onSuccess: async () => {
queryClient.clear();
},
}),
{}
);
};

export const useUserRemoveRole = ({ userId }: { userId: string }) => {
const apiClient = getApiClient();
const queryClient = useQueryClient();
Expand Down
102 changes: 102 additions & 0 deletions packages/web-main/src/routes/_auth/domain.select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Card, Chip, Company, styled } from '@takaro/lib-components';
import { DomainOutputDTO } from '@takaro/apiclient';
import { createFileRoute, useNavigate } from '@tanstack/react-router';
import { useUserSetSelectedDomain, userMeQueryOptions } from 'queries/user';
import { MdDomain as DomainIcon } from 'react-icons/md';
import { AiOutlineArrowRight as ArrowRightIcon } from 'react-icons/ai';

export const TAKARO_DOMAIN_COOKIE_REGEX = /(?:(?:^|.*;\s*)takaro-domain\s*\=\s*([^;]*).*$)|^.*$/;

const Container = styled.div`
padding: ${({ theme }) => theme.spacing[4]};
height: 100vh;
`;

const DomainCardList = styled.div`
display: flex;
align-items: flex-start;
justify-content: center;
flex-direction: column;
margin: auto;
width: 450px;
gap: ${({ theme }) => theme.spacing[2]};
height: 85vh;
`;

export const CardBody = styled.div`
display: flex;
flex-direction: column;
justify-content: space-between;
width: 500px;
height: 100px;
`;

export const Route = createFileRoute('/_auth/domain/select')({
component: Component,
loader: async ({ context }) => {
return await context.queryClient.ensureQueryData(userMeQueryOptions());
},
});

function Component() {
const me = Route.useLoaderData();
const currentDomain = document.cookie.replace(TAKARO_DOMAIN_COOKIE_REGEX, '$1');

// Keep current domain at the top
me.domains.sort((a, b) => {
if (a.id === currentDomain) {
return -1;
}
if (b.id === currentDomain) {
return 1;
}
return 0;
});

return (
<Container>
<Company />
<DomainCardList>
<h2>Select a domain:</h2>
{me.domains.map((domain) => (
<>
<DomainCard domain={domain} isCurrentDomain={currentDomain === domain.id} />
</>
))}
</DomainCardList>
</Container>
);
}

interface DomainCardProps {
domain: DomainOutputDTO;
isCurrentDomain: boolean;
}

function DomainCard({ domain, isCurrentDomain }: DomainCardProps) {
const navigate = useNavigate();
const { mutate } = useUserSetSelectedDomain();

const handleClick = () => {
if (isCurrentDomain === false) {
mutate({ domainId: domain.id });
}
navigate({ to: '/dashboard' });
};

return (
<Card role="link" onClick={handleClick}>
<CardBody>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<DomainIcon size={30} />
{isCurrentDomain && <Chip variant="outline" color="primary" label="current domain" />}
</div>
<h2 style={{ display: 'flex', alignItems: 'center' }}>
{domain.name}
<ArrowRightIcon size={18} style={{ marginLeft: '10px' }} />
</h2>
<div></div>
</CardBody>
</Card>
);
}
33 changes: 18 additions & 15 deletions scripts/dev-data.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -84,40 +84,44 @@ async function resolveCustomModuleConfig(mod) {
}

async function main() {
const userEmail = `${process.env.TAKARO_DEV_USER_NAME}@${process.env.TAKARO_DEV_DOMAIN_NAME}`;

const domainRes = await adminClient.domain.domainControllerCreate({
const domain1Result = await adminClient.domain.domainControllerCreate({
name: process.env.TAKARO_DEV_DOMAIN_NAME,
});

console.log(`Created a domain with id ${domainRes.data.data.createdDomain.id}`);
console.log(`Root user: ${domainRes.data.data.rootUser.email} / ${domainRes.data.data.password}`);
const domain2Result = await adminClient.domain.domainControllerCreate({
name: `${process.env.TAKARO_DEV_DOMAIN_NAME}2`,
});

console.log(`Created domain1 with id ${domain1Result.data.data.createdDomain.id}`);
await addDataToDomain(domain1Result.data.data);

console.log(`Created domain2 with id ${domain2Result.data.data.createdDomain.id}`);
await addDataToDomain(domain2Result.data.data);

console.log(`Root user: ${domain1Result.data.data.rootUser.email} / ${domain1Result.data.data.password}`);
}

async function addDataToDomain(domain) {
const client = new Client({
url: process.env.TAKARO_HOST,
auth: {
username: domainRes.data.data.rootUser.email,
password: domainRes.data.data.password,
username: domain.rootUser.email,
password: domain.password,
},
log: false,
});

await client.login();

const userEmail = `${process.env.TAKARO_DEV_USER_NAME}@${process.env.TAKARO_DEV_DOMAIN_NAME}`;
const userRes = await client.user.userControllerCreate({
email: userEmail,
password: process.env.TAKARO_DEV_USER_PASSWORD,
name: process.env.TAKARO_DEV_USER_NAME,
});

console.log(`Created a user: ${userRes.data.data.email} / ${process.env.TAKARO_DEV_USER_PASSWORD}`);
await client.user.userControllerAssignRole(userRes.data.data.id, domain.rootRole.id);

await client.user.userControllerAssignRole(userRes.data.data.id, domainRes.data.data.rootRole.id);
/*
await client.settings.settingsControllerSet('commandPrefix', {
value: '&'
});
*/
const gameserver = (
await client.gameserver.gameServerControllerCreate({
name: 'Test server',
Expand All @@ -127,7 +131,6 @@ async function main() {
}),
})
).data.data;

console.log(`Created a mock gameserver with id ${gameserver.id}`);

const modules = (await client.module.moduleControllerSearch()).data.data;
Expand Down

0 comments on commit 5afa842

Please sign in to comment.