diff --git a/packages/web-main/src/components/Navbar/UserDropdown.tsx b/packages/web-main/src/components/Navbar/UserDropdown.tsx index 1597674484..58694463d9 100644 --- a/packages/web-main/src/components/Navbar/UserDropdown.tsx +++ b/packages/web-main/src/components/Navbar/UserDropdown.tsx @@ -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; @@ -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
could not get session
; @@ -79,6 +86,12 @@ export const UserDropdown = () => { label="Profile" icon={} /> + navigate({ to: '/domain/select' })} + label="Switch domain" + disabled={!hasMultipleDomains} + icon={} + /> await logOut()} label="Logout" icon={} /> diff --git a/packages/web-main/src/components/Navbar/index.tsx b/packages/web-main/src/components/Navbar/index.tsx index 6b8f04d08a..c3fd1c6497 100644 --- a/packages/web-main/src/components/Navbar/index.tsx +++ b/packages/web-main/src/components/Navbar/index.tsx @@ -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'; @@ -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[] = [ { @@ -129,6 +130,7 @@ interface NavbarProps { export const Navbar: FC = ({ showGameServerNav }) => { const { hasPermission } = useHasPermission([PERMISSIONS.ReadGameservers]); + const theme = useTheme(); return ( @@ -141,6 +143,16 @@ export const Navbar: FC = ({ showGameServerNav }) => {
+
+ Domain: + +
+ diff --git a/packages/web-main/src/hooks/useAuth.tsx b/packages/web-main/src/hooks/useAuth.tsx index a2a8fe8dd5..e27813c1b4 100644 --- a/packages/web-main/src/hooks/useAuth.tsx +++ b/packages/web-main/src/hooks/useAuth.tsx @@ -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; diff --git a/packages/web-main/src/queries/user.tsx b/packages/web-main/src/queries/user.tsx index c01c4f568c..c2d065f863 100644 --- a/packages/web-main/src/queries/user.tsx +++ b/packages/web-main/src/queries/user.tsx @@ -3,6 +3,7 @@ import { getApiClient } from 'util/getApiClient'; import { APIOutput, IdUuidDTO, + MeOutoutDTO, UserOutputArrayDTOAPI, UserOutputWithRolesDTO, UserSearchInputDTO, @@ -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 { @@ -34,6 +36,12 @@ export const userQueryOptions = (userId: string) => queryFn: async () => (await getApiClient().user.userControllerGetOne(userId)).data.data, }); +export const userMeQueryOptions = () => + queryOptions>({ + queryKey: userKeys.me(), + queryFn: async () => (await getApiClient().user.userControllerMe()).data.data, + }); + interface IUserRoleAssign { userId: string; roleId: string; @@ -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( + useMutation, 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(); diff --git a/packages/web-main/src/routes/_auth/domain.select.tsx b/packages/web-main/src/routes/_auth/domain.select.tsx new file mode 100644 index 0000000000..c6a2dbfc06 --- /dev/null +++ b/packages/web-main/src/routes/_auth/domain.select.tsx @@ -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 ( + + + +

Select a domain:

+ {me.domains.map((domain) => ( + <> + + + ))} +
+
+ ); +} + +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 ( + + +
+ + {isCurrentDomain && } +
+

+ {domain.name} + +

+
+
+
+ ); +} diff --git a/scripts/dev-data.mjs b/scripts/dev-data.mjs index 78239f75f0..1f4d9b4af0 100755 --- a/scripts/dev-data.mjs +++ b/scripts/dev-data.mjs @@ -84,26 +84,35 @@ 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, @@ -111,13 +120,8 @@ async function main() { }); 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', @@ -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;