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;