Skip to content

Commit

Permalink
User role assign stuff (#617)
Browse files Browse the repository at this point in the history
* parking user role assign stuff

* fix: code style

* chore: add tests for user role assignments

---------

Co-authored-by: takaro-ci-bot[bot] <138661031+takaro-ci-bot[bot]@users.noreply.github.com>
  • Loading branch information
niekcandaele and takaro-ci-bot[bot] authored Sep 2, 2023
1 parent a59613c commit d4fbe89
Show file tree
Hide file tree
Showing 17 changed files with 404 additions and 65 deletions.
14 changes: 10 additions & 4 deletions packages/app-api/src/controllers/UserController.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { IsEmail, IsOptional, IsString, IsUUID, Length, ValidateNested } from 'class-validator';
import { ITakaroQuery } from '@takaro/db';
import { APIOutput, apiResponse } from '@takaro/http';
import { UserCreateInputDTO, UserOutputDTO, UserService, UserUpdateDTO } from '../service/UserService.js';
import {
UserCreateInputDTO,
UserOutputDTO,
UserOutputWithRolesDTO,
UserService,
UserUpdateDTO,
} from '../service/UserService.js';
import { AuthenticatedRequest, AuthService, LoginOutputDTO } from '../service/AuthService.js';
import { Body, Get, Post, Delete, JsonController, UseBefore, Req, Put, Params, Res } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
Expand Down Expand Up @@ -41,10 +47,10 @@ class LoginOutputDTOAPI extends APIOutput<LoginOutputDTO> {
declare data: LoginOutputDTO;
}

class UserOutputDTOAPI extends APIOutput<UserOutputDTO> {
@Type(() => UserOutputDTO)
class UserOutputDTOAPI extends APIOutput<UserOutputWithRolesDTO> {
@Type(() => UserOutputWithRolesDTO)
@ValidateNested()
declare data: UserOutputDTO;
declare data: UserOutputWithRolesDTO;
}

class UserOutputArrayDTOAPI extends APIOutput<UserOutputDTO[]> {
Expand Down
4 changes: 4 additions & 0 deletions packages/e2e/src/web-main/fixtures/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
HookCreateDTOEventTypeEnum,
ModuleOutputDTO,
PlayerOutputDTO,
UserOutputDTO,
} from '@takaro/apiclient';
import humanId from 'human-id/dist/index.js';
import { GameServersPage } from './GameServersPage.js';
Expand Down Expand Up @@ -51,6 +52,7 @@ interface IFixtures {
gameServer: GameServerOutputDTO;
mailhog: MailhogAPI;
players: PlayerOutputDTO[];
rootUser: UserOutputDTO;
};
}

Expand Down Expand Up @@ -106,6 +108,7 @@ export const basicTest = base.extend<IFixtures>({
moduleDefinitionsPage: new ModuleDefinitionsPage(page),
mailhog,
players: [],
rootUser: data.rootUser,
});

// fixture teardown
Expand Down Expand Up @@ -201,6 +204,7 @@ export const test = base.extend<IFixtures>({
mailhog,
gameServer: gameServer.data.data,
players: players.data.data,
rootUser: data.rootUser,
});

// fixture teardown
Expand Down
51 changes: 51 additions & 0 deletions packages/e2e/src/web-main/pages/users.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import playwright from '@playwright/test';
import { test } from '../fixtures/index.js';

const { expect } = playwright;

test('Can view users', async ({ page, takaro }) => {
await page.getByRole('link', { name: 'Users' }).click();
await expect(page.getByText(takaro.rootUser.email)).toBeVisible();
});

test.describe('Role assignment', () => {
test('Can assign a role to a user', async ({ page }) => {
await page.getByRole('link', { name: 'Users' }).click();

await page.getByRole('button', { name: 'user-actions' }).click();
await page.getByRole('menuitem', { name: 'Go to user profile' }).click();

await page.getByRole('button', { name: 'Assign role' }).click();

await page.locator('#roleId').click();
await page.getByRole('option', { name: 'Player' }).click();

await page.getByRole('button', { name: 'Save changes' }).click();

await expect(page.getByRole('cell', { name: 'Player', exact: true })).toBeVisible();
});

test('Can remove a role from a user', async ({ page }) => {
await page.getByRole('link', { name: 'Users' }).click();

await page.getByRole('button', { name: 'user-actions' }).click();
await page.getByRole('menuitem', { name: 'Go to user profile' }).click();

await page.getByRole('button', { name: 'Assign role' }).click();

await page.locator('#roleId').click();
await page.getByRole('option', { name: 'Player' }).click();

await page.getByRole('button', { name: 'Save changes' }).click();

await expect(page.getByRole('cell', { name: 'Player', exact: true })).toBeVisible();

await page
.getByRole('row', { name: 'Player player-actions' })
.getByRole('button', { name: 'player-actions' })
.click();
await page.getByRole('menuitem', { name: 'Unassign role' }).click();

await expect(page.getByRole('cell', { name: 'Player', exact: true })).not.toBeVisible();
});
});
Original file line number Diff line number Diff line change
@@ -1 +1 @@
6.6.0
6.0.1
24 changes: 17 additions & 7 deletions packages/lib-apiclient/src/generated/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@
* Do not edit the class manually.
*/

import type { Configuration } from './configuration.js';
import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios';
import globalAxios from 'axios';
import { Configuration } from './configuration.js';
import globalAxios, { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios';
// Some imports not used depending on template conditions
// @ts-ignore
import {
Expand All @@ -29,9 +28,8 @@ import {
toPathString,
createRequestFunction,
} from './common.js';
import type { RequestArgs } from './base.js';
// @ts-ignore
import { BASE_PATH, COLLECTION_FORMATS, BaseAPI, RequiredError } from './base.js';
import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError } from './base.js';

/**
*
Expand Down Expand Up @@ -1237,6 +1235,12 @@ export interface EventCreateDTO {
* @memberof EventCreateDTO
*/
playerId?: string;
/**
*
* @type {string}
* @memberof EventCreateDTO
*/
userId?: string;
/**
*
* @type {string}
Expand Down Expand Up @@ -1349,6 +1353,12 @@ export interface EventOutputDTO {
* @memberof EventOutputDTO
*/
playerId?: string;
/**
*
* @type {string}
* @memberof EventOutputDTO
*/
userId?: string;
/**
*
* @type {string}
Expand Down Expand Up @@ -4968,10 +4978,10 @@ export interface UserOutputDTO {
export interface UserOutputDTOAPI {
/**
*
* @type {UserOutputDTO}
* @type {UserOutputWithRolesDTO}
* @memberof UserOutputDTOAPI
*/
data: UserOutputDTO;
data: UserOutputWithRolesDTO;
/**
*
* @type {MetadataOutput}
Expand Down
7 changes: 3 additions & 4 deletions packages/lib-apiclient/src/generated/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@
* Do not edit the class manually.
*/

import type { Configuration } from './configuration.js';
import { Configuration } from './configuration.js';
// Some imports not used depending on template conditions
// @ts-ignore
import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios';
import globalAxios from 'axios';
import globalAxios, { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios';

export const BASE_PATH = 'http://localhost'.replace(/\/+$/, '');

Expand Down Expand Up @@ -68,8 +67,8 @@ export class BaseAPI {
* @extends {Error}
*/
export class RequiredError extends Error {
name: 'RequiredError' = 'RequiredError';
constructor(public field: string, msg?: string) {
super(msg);
this.name = 'RequiredError';
}
}
26 changes: 3 additions & 23 deletions packages/lib-apiclient/src/generated/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@
* Do not edit the class manually.
*/

import type { Configuration } from './configuration.js';
import type { RequestArgs } from './base.js';
import type { AxiosInstance, AxiosResponse } from 'axios';
import { RequiredError } from './base.js';
import { Configuration } from './configuration.js';
import { RequiredError, RequestArgs } from './base.js';
import { AxiosInstance, AxiosResponse } from 'axios';

/**
*
Expand Down Expand Up @@ -94,25 +93,6 @@ export const setOAuthToObject = async function (
}
};

function setFlattenedQueryParams(urlSearchParams: URLSearchParams, parameter: any, key: string = ''): void {
if (parameter == null) return;
if (typeof parameter === 'object') {
if (Array.isArray(parameter)) {
(parameter as any[]).forEach((item) => setFlattenedQueryParams(urlSearchParams, item, key));
} else {
Object.keys(parameter).forEach((currentKey) =>
setFlattenedQueryParams(urlSearchParams, parameter[currentKey], `${key}${key !== '' ? '.' : ''}${currentKey}`)
);
}
} else {
if (urlSearchParams.has(key)) {
urlSearchParams.append(key, parameter);
} else {
urlSearchParams.set(key, parameter);
}
}
}

/**
*
* @export
Expand Down
14 changes: 10 additions & 4 deletions packages/web-main/src/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ import { Roles } from './pages/roles';
import { RolesCreate } from './pages/roles/RolesCreate';
import { RolesUpdate } from './pages/roles/RolesUpdate';
import { PlayerProfile } from 'pages/player/profile';
import { AssignRole } from 'pages/roles/assignRole';
import { AssignPlayerRole } from 'pages/roles/assignPlayerRole';
import { UserProfile } from 'pages/users/profile';
import { AssignUserRole } from 'pages/roles/assignUserRole';

const SentryRoutes = withSentryReactRouterV6Routing(Routes);

Expand Down Expand Up @@ -69,14 +71,18 @@ export const Router: FC = () => (
<Route element={<DiscordSettings />} path={PATHS.settings.discordSettings()} />
</Route>

<Route element={<Players />} path={PATHS.players()} />

<Route element={<PlayerProfile />} path={PATHS.player.profile(':playerId')}>
<Route element={<AssignRole />} path={PATHS.player.assignRole(':playerId')} />
<Route element={<AssignPlayerRole />} path={PATHS.player.assignRole(':playerId')} />
</Route>

<Route element={<GameServers />} path="/server/" />

<Route element={<Users />} path={PATHS.users()} />

<Route element={<UserProfile />} path={PATHS.user.profile(':userId')}>
<Route element={<AssignUserRole />} path={PATHS.user.assignRole(':userId')} />
</Route>

<Route element={<Variables />} path={PATHS.variables()} />

{/* ======================== CRUD Game Servers ======================== */}
Expand Down
2 changes: 1 addition & 1 deletion packages/web-main/src/pages/Users.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ const Users: FC = () => {
<Dropdown.Menu.Item
label="Go to user profile"
icon={<ProfileIcon />}
onClick={() => navigate(`${PATHS.users()}/${info.row.original.id}`)}
onClick={() => navigate(`${PATHS.user.profile(info.row.original.id)}`)}
/>
</Dropdown.Menu.Group>
<Dropdown.Menu.Item label="Edit roles" icon={<EditIcon />} onClick={() => navigate('')} />
Expand Down
4 changes: 2 additions & 2 deletions packages/web-main/src/pages/player/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { createColumnHelper } from '@tanstack/react-table';
import { useGameServers } from 'queries/gameservers';
import { DateTime } from 'luxon';
import { AiOutlineDelete as DeleteIcon, AiOutlineRight as ActionIcon } from 'react-icons/ai';
import { useRoleUnassign } from 'queries/roles';
import { usePlayerRoleUnassign } from 'queries/roles';

export const PlayerProfile: FC = () => {
const { playerId } = useParams<{ playerId: string }>();
Expand Down Expand Up @@ -59,7 +59,7 @@ interface IPlayerRolesTableProps {

const PlayerRolesTable: FC<IPlayerRolesTableProps> = ({ roles, playerId }) => {
const { pagination, columnFilters, sorting, columnSearch } = useTableActions<RoleAssignmentOutputDTO>();
const { mutate } = useRoleUnassign();
const { mutate } = usePlayerRoleUnassign();

const filteredServerIds = roles.filter((role) => role.gameServerId).map((role) => role.gameServerId);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Drawer, CollapseList, FormError, Button, Select, TextField, Loading } from '@takaro/lib-components';
import { PATHS } from 'paths';
import { useRoleAssign, useRoles } from 'queries/roles';
import { usePlayerRoleAssign, useRoles } from 'queries/roles';
import { FC, useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { ButtonContainer } from './style';
Expand All @@ -11,7 +11,7 @@ import { useGameServers } from 'queries/gameservers';
import { GameServerOutputDTO, RoleOutputDTO } from '@takaro/apiclient';

interface IFormInputs {
playerId: string;
id: string;
roleId: string;
gameServerId?: string;
}
Expand All @@ -21,7 +21,7 @@ interface IAssignRoleFormProps {
gameServers: GameServerOutputDTO[];
}

export const AssignRole: FC = () => {
export const AssignPlayerRole: FC = () => {
const { data: roles, isLoading: isLoadingRoles } = useRoles();
const { data: gameservers, isLoading: isLoadingGameServers } = useGameServers();

Expand All @@ -40,7 +40,7 @@ export const AssignRole: FC = () => {

const AssignRoleForm: FC<IAssignRoleFormProps> = ({ roles, gameServers }) => {
const [open, setOpen] = useState(true);
const { mutateAsync, isLoading, error } = useRoleAssign();
const { mutateAsync, isLoading, error } = usePlayerRoleAssign();
const navigate = useNavigate();
const { playerId } = useParams<{ playerId: string }>();

Expand All @@ -59,16 +59,16 @@ const AssignRoleForm: FC<IAssignRoleFormProps> = ({ roles, gameServers }) => {
mode: 'onSubmit',
resolver: zodResolver(roleAssignValidationSchema),
defaultValues: {
playerId,
id: playerId,
roleId: roles[0].id,
gameServerId: gameServers[0].id,
},
});

const onSubmit: SubmitHandler<IFormInputs> = async ({ playerId, roleId, gameServerId }) => {
const onSubmit: SubmitHandler<IFormInputs> = async ({ id, roleId, gameServerId }) => {
if (gameServerId === 'null') gameServerId = undefined;
await mutateAsync({ id: playerId, roleId, gameServerId });
navigate(PATHS.player.profile(playerId));
await mutateAsync({ id, roleId, gameServerId });
navigate(PATHS.player.profile(id));
};

return (
Expand All @@ -79,7 +79,7 @@ const AssignRoleForm: FC<IAssignRoleFormProps> = ({ roles, gameServers }) => {
<CollapseList>
<form onSubmit={handleSubmit(onSubmit)} id="create-role-form">
<CollapseList.Item title="General">
<TextField readOnly control={control} name="playerId" label="Player" />
<TextField readOnly control={control} name="id" label="Player" />

<Select
control={control}
Expand Down
Loading

0 comments on commit d4fbe89

Please sign in to comment.