feat: change global role on administration page (#61)
* feat: change global role on administration page * feat: change global role on administration page test code todo * feat: change global role on administration page test code * feat: change global role on administration page * feat: change global role on administration page * feat: change global role on administration page * feat: change global role on administration page * feat: change global role on administration page
This commit is contained in:
@@ -49,6 +49,11 @@ app.use('/users', usersFindRoute);
|
|||||||
app.use('/users', usersSearchRoute);
|
app.use('/users', usersSearchRoute);
|
||||||
app.use('/users', signUpRoute);
|
app.use('/users', signUpRoute);
|
||||||
app.use('/users', signInRoute);
|
app.use('/users', signInRoute);
|
||||||
|
// ESM import
|
||||||
|
(async () => {
|
||||||
|
const updateRoute = await import('./routes/users/update.mjs');
|
||||||
|
app.use('/users', updateRoute.default(sequelize));
|
||||||
|
})();
|
||||||
|
|
||||||
// "/projects"
|
// "/projects"
|
||||||
const projectsIndexRoute = require('./routes/projects/index')(sequelize);
|
const projectsIndexRoute = require('./routes/projects/index')(sequelize);
|
||||||
|
|||||||
58
backend/routes/users/update.mjs
Normal file
58
backend/routes/users/update.mjs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { DataTypes } from 'sequelize';
|
||||||
|
import defineUser from '../../models/users.js';
|
||||||
|
import { roles } from './authSettings.js';
|
||||||
|
import authMiddleware from '../../middleware/auth.js';
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
export default function (sequelize) {
|
||||||
|
const { verifySignedIn, verifyAdmin } = authMiddleware(sequelize);
|
||||||
|
const User = defineUser(sequelize, DataTypes);
|
||||||
|
|
||||||
|
router.put('/:userId', verifySignedIn, verifyAdmin, async (req, res) => {
|
||||||
|
// param check
|
||||||
|
const userId = req.params.userId;
|
||||||
|
if (!userId) {
|
||||||
|
return res.status(400).send('userId is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { newRole } = req.body;
|
||||||
|
if (newRole === undefined || newRole === null) {
|
||||||
|
return res.status(400).send('newRole is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const targetUser = await User.findByPk(userId);
|
||||||
|
if (!targetUser) {
|
||||||
|
return res.status(404).send('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminRoleIndex = roles.findIndex((entry) => entry.uid === 'administrator');
|
||||||
|
const userRoleIndex = roles.findIndex((entry) => entry.uid === 'user');
|
||||||
|
|
||||||
|
// At least one administrator is required.
|
||||||
|
if (newRole === userRoleIndex) {
|
||||||
|
const adminCount = await User.count({
|
||||||
|
where: {
|
||||||
|
role: adminRoleIndex,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (adminCount <= 1) {
|
||||||
|
return res.status(400).send('At least one administrator is required.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await targetUser.update({
|
||||||
|
role: newRole,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ user: targetUser });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
106
backend/routes/users/update.test.js
Normal file
106
backend/routes/users/update.test.js
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import request from 'supertest';
|
||||||
|
import express from 'express';
|
||||||
|
import updateRoute from './update';
|
||||||
|
import { Sequelize } from 'sequelize';
|
||||||
|
import { roles } from './authSettings';
|
||||||
|
|
||||||
|
const adminRoleIndex = roles.findIndex((entry) => entry.uid === 'administrator');
|
||||||
|
const userRoleIndex = roles.findIndex((entry) => entry.uid === 'user');
|
||||||
|
|
||||||
|
// mock of authentication middleware
|
||||||
|
vi.mock('../../middleware/auth', () => ({
|
||||||
|
default: () => ({
|
||||||
|
verifySignedIn: vi.fn((req, res, next) => {
|
||||||
|
req.userId = 1; // Mock user ID
|
||||||
|
next();
|
||||||
|
}),
|
||||||
|
verifyAdmin: vi.fn((req, res, next) => {
|
||||||
|
next(); // Allow all as admin
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// mock defineUser
|
||||||
|
const mockUser = {
|
||||||
|
findByPk: vi.fn(),
|
||||||
|
count: vi.fn(),
|
||||||
|
update: vi.fn(),
|
||||||
|
};
|
||||||
|
vi.mock('../../models/users', () => ({
|
||||||
|
default: (sequelize, DataTypes) => mockUser,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// test
|
||||||
|
describe('updateUserRole', () => {
|
||||||
|
let app;
|
||||||
|
const sequelize = new Sequelize({
|
||||||
|
dialect: 'sqlite',
|
||||||
|
logging: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Mount the update route
|
||||||
|
app.use('/users', updateRoute(sequelize));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('call update API without new role', async () => {
|
||||||
|
const response = await request(app).put('/users/2').send();
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
expect(response.text).toBe('newRole is required');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('promote not existing user to admin will return 404', async () => {
|
||||||
|
mockUser.findByPk.mockResolvedValue(null); // No user found
|
||||||
|
const response = await request(app).put('/users/2').send({
|
||||||
|
newRole: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(404);
|
||||||
|
expect(response.text).toBe('User not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('promote existing user to admin will return 200', async () => {
|
||||||
|
const targetUser = { id: 2, role: userRoleIndex, update: vi.fn() }; // Normal user
|
||||||
|
mockUser.findByPk.mockResolvedValue(targetUser);
|
||||||
|
|
||||||
|
const response = await request(app).put('/users/2').send({
|
||||||
|
newRole: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(targetUser.update).toHaveBeenCalledWith({ role: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 400 if trying to demote the only administrator', async () => {
|
||||||
|
const targetUser = { id: 1, role: adminRoleIndex, update: vi.fn() }; // Administrator
|
||||||
|
mockUser.findByPk.mockResolvedValue(targetUser);
|
||||||
|
mockUser.count.mockResolvedValue(1); // Only one admin
|
||||||
|
|
||||||
|
const response = await request(app).put('/users/1').send({
|
||||||
|
newRole: 1,
|
||||||
|
}); // Downgrading admin to user
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
expect(response.text).toBe('At least one administrator is required.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle internal server errors', async () => {
|
||||||
|
// Suppress error logging to console
|
||||||
|
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
|
// Simulate DB error
|
||||||
|
mockUser.findByPk.mockRejectedValue(new Error('Database error'));
|
||||||
|
|
||||||
|
const response = await request(app).put('/users/1').send({
|
||||||
|
newRole: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(500);
|
||||||
|
expect(response.text).toBe('Internal Server Error');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -122,7 +122,14 @@
|
|||||||
"role": "Role",
|
"role": "Role",
|
||||||
"administrator": "Administrator",
|
"administrator": "Administrator",
|
||||||
"user": "User",
|
"user": "User",
|
||||||
"no_users_found": "No users found"
|
"no_users_found": "No users found",
|
||||||
|
"quit_admin": "Quit Administrator",
|
||||||
|
"close": "Close",
|
||||||
|
"quit": "Quit",
|
||||||
|
"quit_confirm": "If you relinquish your administrator privileges, you will no longer be able to access the administration page.",
|
||||||
|
"role_changed": "Role changed",
|
||||||
|
"lost_admin_auth": "Lost admin authority",
|
||||||
|
"at_least": "At least one administrator is required"
|
||||||
},
|
},
|
||||||
"Projects": {
|
"Projects": {
|
||||||
"projectList": "Project List",
|
"projectList": "Project List",
|
||||||
|
|||||||
@@ -123,7 +123,14 @@
|
|||||||
"role": "ロール",
|
"role": "ロール",
|
||||||
"administrator": "管理者",
|
"administrator": "管理者",
|
||||||
"user": "ユーザー",
|
"user": "ユーザー",
|
||||||
"no_users_found": "ユーザーがいません"
|
"no_users_found": "ユーザーがいません",
|
||||||
|
"quit_admin": "管理者を辞める",
|
||||||
|
"close": "閉じる",
|
||||||
|
"quit": "辞める",
|
||||||
|
"quit_confirm": "管理者権限を放棄すると管理ページにアクセスできなくなります。",
|
||||||
|
"role_changed": "ロールを更新しました",
|
||||||
|
"lost_admin_auth": "管理者権限を失いました",
|
||||||
|
"at_least": "最低1人以上の管理者が必要です。"
|
||||||
},
|
},
|
||||||
"Projects": {
|
"Projects": {
|
||||||
"projectList": "プロジェクト一覧",
|
"projectList": "プロジェクト一覧",
|
||||||
|
|||||||
@@ -3,9 +3,15 @@ import React from 'react';
|
|||||||
import { useState, useEffect, useContext } from 'react';
|
import { useState, useEffect, useContext } from 'react';
|
||||||
import { UserType, AdminMessages } from '@/types/user';
|
import { UserType, AdminMessages } from '@/types/user';
|
||||||
import { TokenContext } from '@/utils/TokenProvider';
|
import { TokenContext } from '@/utils/TokenProvider';
|
||||||
|
import { ToastContext } from '@/utils/ToastProvider';
|
||||||
|
import { useRouter } from '@/src/navigation';
|
||||||
import UsersTable from './UsersTable';
|
import UsersTable from './UsersTable';
|
||||||
import Config from '@/config/config';
|
import Config from '@/config/config';
|
||||||
import { LocaleCodeType } from '@/types/locale';
|
import { LocaleCodeType } from '@/types/locale';
|
||||||
|
import { updateUserRole } from '@/utils/usersControl';
|
||||||
|
import { Button } from '@nextui-org/react';
|
||||||
|
import { roles } from '@/config/selection';
|
||||||
|
import DeleteConfirmDialog from '@/components/DeleteConfirmDialog';
|
||||||
const apiServer = Config.apiServer;
|
const apiServer = Config.apiServer;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -37,33 +43,96 @@ async function fetchUsers(jwt: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function AdminPage({ messages, locale }: Props) {
|
export default function AdminPage({ messages, locale }: Props) {
|
||||||
const context = useContext(TokenContext);
|
const router = useRouter();
|
||||||
|
const tokenContext = useContext(TokenContext);
|
||||||
|
const toastContext = useContext(ToastContext);
|
||||||
const [users, setUsers] = useState<UserType[]>([]);
|
const [users, setUsers] = useState<UserType[]>([]);
|
||||||
|
const [myself, setMyself] = useState<UserType | null>(null);
|
||||||
|
|
||||||
|
// Quit confirm dialog
|
||||||
|
const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false);
|
||||||
|
const closeConfirmDialog = () => {
|
||||||
|
setIsConfirmDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchDataEffect() {
|
async function fetchDataEffect() {
|
||||||
if (!context.isSignedIn()) {
|
if (!tokenContext.isAdmin()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await fetchUsers(context.token.access_token);
|
const data = await fetchUsers(tokenContext.token.access_token);
|
||||||
setUsers(data);
|
setUsers(data);
|
||||||
|
|
||||||
|
if (tokenContext.token.user) {
|
||||||
|
setMyself(tokenContext.token.user);
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error in effect:', error.message);
|
console.error('Error in effect:', error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchDataEffect();
|
fetchDataEffect();
|
||||||
}, [context]);
|
}, [tokenContext]);
|
||||||
|
|
||||||
|
const handleChangeRole = async (userEdit: UserType, role: number) => {
|
||||||
|
if (!tokenContext.isAdmin()) {
|
||||||
|
console.error('you are not admin');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userEdit.id) {
|
||||||
|
const data = await updateUserRole(tokenContext.token.access_token, userEdit.id, role);
|
||||||
|
if (data.user) {
|
||||||
|
toastContext.showToast(messages.roleChanged, 'dark');
|
||||||
|
setUsers((prevUsers) => {
|
||||||
|
return prevUsers.map((user) => {
|
||||||
|
if (user.id === userEdit.id) {
|
||||||
|
return { ...user, role: role };
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onQuitConfirm = async () => {
|
||||||
|
if (myself && myself.id) {
|
||||||
|
const userRoleIndex = roles.findIndex((entry) => entry.uid === 'user');
|
||||||
|
const data = await updateUserRole(tokenContext.token.access_token, myself.id, userRoleIndex);
|
||||||
|
|
||||||
|
if (data && data.user) {
|
||||||
|
toastContext.showToast(messages.lostAdminAuth, 'dark');
|
||||||
|
router.push(`/`, { locale: locale });
|
||||||
|
} else {
|
||||||
|
toastContext.showToast(messages.atLeast, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto max-w-3xl pt-16 px-6 flex-grow">
|
<>
|
||||||
<div className="w-full p-3 flex items-center justify-between">
|
<div className="container mx-auto max-w-3xl pt-16 px-6 flex-grow">
|
||||||
<h3 className="font-bold">{messages.userManagement}</h3>
|
<div className="w-full p-3 flex items-center justify-between">
|
||||||
|
<h3 className="font-bold">{messages.userManagement}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UsersTable users={users} myself={myself} onChangeRole={handleChangeRole} messages={messages} />
|
||||||
|
<Button className="mt-4" color="danger" variant="bordered" onPress={() => setIsConfirmDialogOpen(true)}>
|
||||||
|
{messages.quitAdmin}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UsersTable users={users} messages={messages} />
|
<DeleteConfirmDialog
|
||||||
</div>
|
isOpen={isConfirmDialogOpen}
|
||||||
|
onCancel={closeConfirmDialog}
|
||||||
|
onConfirm={onQuitConfirm}
|
||||||
|
closeText={messages.close}
|
||||||
|
confirmText={messages.quitConfirm}
|
||||||
|
deleteText={messages.quit}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,31 @@
|
|||||||
import { useState, useMemo, useCallback } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, SortDescriptor } from '@nextui-org/react';
|
import {
|
||||||
|
Button,
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableColumn,
|
||||||
|
TableBody,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
SortDescriptor,
|
||||||
|
DropdownTrigger,
|
||||||
|
Dropdown,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownItem,
|
||||||
|
} from '@nextui-org/react';
|
||||||
|
import { ChevronDown } from 'lucide-react';
|
||||||
import { UserType, AdminMessages } from '@/types/user';
|
import { UserType, AdminMessages } from '@/types/user';
|
||||||
import { roles } from '@/config/selection';
|
import { roles } from '@/config/selection';
|
||||||
import Avatar from 'boring-avatars';
|
import Avatar from 'boring-avatars';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
users: UserType[];
|
users: UserType[];
|
||||||
|
myself: UserType | null;
|
||||||
|
onChangeRole: (userEdit: UserType, role: number) => void;
|
||||||
messages: AdminMessages;
|
messages: AdminMessages;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function UsersTable({ users, messages }: Props) {
|
export default function UsersTable({ users, myself, onChangeRole, messages }: Props) {
|
||||||
const headerColumns = [
|
const headerColumns = [
|
||||||
{ name: messages.avatar, uid: 'avatar', sortable: false },
|
{ name: messages.avatar, uid: 'avatar', sortable: false },
|
||||||
{ name: messages.id, uid: 'id', sortable: true },
|
{ name: messages.id, uid: 'id', sortable: true },
|
||||||
@@ -33,7 +49,15 @@ export default function UsersTable({ users, messages }: Props) {
|
|||||||
});
|
});
|
||||||
}, [sortDescriptor, users]);
|
}, [sortDescriptor, users]);
|
||||||
|
|
||||||
const renderCell = useCallback((user: UserType, columnKey: string) => {
|
const isMyself = (myself: UserType | null, user: UserType) => {
|
||||||
|
if (myself && myself.id === user.id) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderCell = (user: UserType, columnKey: string) => {
|
||||||
const cellValue = user[columnKey as keyof UserType];
|
const cellValue = user[columnKey as keyof UserType];
|
||||||
|
|
||||||
switch (columnKey) {
|
switch (columnKey) {
|
||||||
@@ -53,12 +77,32 @@ export default function UsersTable({ users, messages }: Props) {
|
|||||||
case 'name':
|
case 'name':
|
||||||
return cellValue;
|
return cellValue;
|
||||||
case 'role':
|
case 'role':
|
||||||
return <span>{messages[roles[cellValue as number].uid]}</span>;
|
return (
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownTrigger>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
isDisabled={isMyself(myself, user)}
|
||||||
|
variant="light"
|
||||||
|
endContent={<ChevronDown size={16} />}
|
||||||
|
>
|
||||||
|
<span className="w-20">{messages[roles[cellValue as number].uid]}</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownTrigger>
|
||||||
|
<DropdownMenu aria-label="global role actions">
|
||||||
|
{roles.map((role, index) => (
|
||||||
|
<DropdownItem key={index} onPress={() => onChangeRole(user, index)}>
|
||||||
|
{messages[role.uid]}
|
||||||
|
</DropdownItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return cellValue;
|
return cellValue;
|
||||||
}
|
}
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const classNames = useMemo(
|
const classNames = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { useTranslations } from 'next-intl';
|
|||||||
import AdminPage from './AdminPage';
|
import AdminPage from './AdminPage';
|
||||||
import { PageType } from '@/types/base';
|
import { PageType } from '@/types/base';
|
||||||
import { LocaleCodeType } from '@/types/locale';
|
import { LocaleCodeType } from '@/types/locale';
|
||||||
|
import { AdminMessages } from '@/types/user';
|
||||||
|
|
||||||
export default function Page({ params }: PageType) {
|
export default function Page({ params }: PageType) {
|
||||||
const t = useTranslations('Admin');
|
const t = useTranslations('Admin');
|
||||||
const messages = {
|
const messages: AdminMessages = {
|
||||||
admin: t('admin'),
|
|
||||||
userManagement: t('user_management'),
|
userManagement: t('user_management'),
|
||||||
avatar: t('avatar'),
|
avatar: t('avatar'),
|
||||||
id: t('id'),
|
id: t('id'),
|
||||||
@@ -16,6 +16,13 @@ export default function Page({ params }: PageType) {
|
|||||||
administrator: t('administrator'),
|
administrator: t('administrator'),
|
||||||
user: t('user'),
|
user: t('user'),
|
||||||
noUsersFound: t('no_users_found'),
|
noUsersFound: t('no_users_found'),
|
||||||
|
quitAdmin: t('quit_admin'),
|
||||||
|
quit: t('quit'),
|
||||||
|
quitConfirm: t('quit_confirm'),
|
||||||
|
close: t('close'),
|
||||||
|
roleChanged: t('role_changed'),
|
||||||
|
lostAdminAuth: t('lost_admin_auth'),
|
||||||
|
atLeast: t('at_least'),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export default function CaseEditor({
|
|||||||
priorityMessages,
|
priorityMessages,
|
||||||
locale,
|
locale,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const toakenContext = useContext(TokenContext);
|
const tokenContext = useContext(TokenContext);
|
||||||
const toastContext = useContext(ToastContext);
|
const toastContext = useContext(ToastContext);
|
||||||
const [testCase, setTestCase] = useState<CaseType>(defaultTestCase);
|
const [testCase, setTestCase] = useState<CaseType>(defaultTestCase);
|
||||||
const [isTitleInvalid, setIsTitleInvalid] = useState<boolean>(false);
|
const [isTitleInvalid, setIsTitleInvalid] = useState<boolean>(false);
|
||||||
@@ -194,11 +194,11 @@ export default function CaseEditor({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchDataEffect() {
|
async function fetchDataEffect() {
|
||||||
if (!toakenContext.isSignedIn()) {
|
if (!tokenContext.isSignedIn()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const data = await fetchCase(toakenContext.token.access_token, Number(caseId));
|
const data = await fetchCase(tokenContext.token.access_token, Number(caseId));
|
||||||
data.Steps.forEach((step: StepType) => {
|
data.Steps.forEach((step: StepType) => {
|
||||||
step.editState = 'notChanged';
|
step.editState = 'notChanged';
|
||||||
});
|
});
|
||||||
@@ -209,7 +209,7 @@ export default function CaseEditor({
|
|||||||
}
|
}
|
||||||
|
|
||||||
fetchDataEffect();
|
fetchDataEffect();
|
||||||
}, [toakenContext]);
|
}, [tokenContext]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -232,14 +232,14 @@ export default function CaseEditor({
|
|||||||
<Button
|
<Button
|
||||||
startContent={<Save size={16} />}
|
startContent={<Save size={16} />}
|
||||||
size="sm"
|
size="sm"
|
||||||
isDisabled={!toakenContext.isProjectDeveloper(Number(projectId))}
|
isDisabled={!tokenContext.isProjectDeveloper(Number(projectId))}
|
||||||
color="primary"
|
color="primary"
|
||||||
isLoading={isUpdating}
|
isLoading={isUpdating}
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
setIsUpdating(true);
|
setIsUpdating(true);
|
||||||
await updateCase(toakenContext.token.access_token, testCase);
|
await updateCase(tokenContext.token.access_token, testCase);
|
||||||
if (testCase.Steps) {
|
if (testCase.Steps) {
|
||||||
await updateSteps(toakenContext.token.access_token, Number(caseId), testCase.Steps);
|
await updateSteps(tokenContext.token.access_token, Number(caseId), testCase.Steps);
|
||||||
}
|
}
|
||||||
|
|
||||||
toastContext.showToast(messages.updatedTestCase, 'dark');
|
toastContext.showToast(messages.updatedTestCase, 'dark');
|
||||||
@@ -387,7 +387,7 @@ export default function CaseEditor({
|
|||||||
<Button
|
<Button
|
||||||
startContent={<Plus size={16} />}
|
startContent={<Plus size={16} />}
|
||||||
size="sm"
|
size="sm"
|
||||||
isDisabled={!toakenContext.isProjectDeveloper(Number(projectId))}
|
isDisabled={!tokenContext.isProjectDeveloper(Number(projectId))}
|
||||||
color="primary"
|
color="primary"
|
||||||
className="ms-3"
|
className="ms-3"
|
||||||
onPress={() => onPlusClick(1)}
|
onPress={() => onPlusClick(1)}
|
||||||
@@ -397,7 +397,7 @@ export default function CaseEditor({
|
|||||||
</div>
|
</div>
|
||||||
{testCase.Steps && (
|
{testCase.Steps && (
|
||||||
<CaseStepsEditor
|
<CaseStepsEditor
|
||||||
isDisabled={!toakenContext.isProjectDeveloper(Number(projectId))}
|
isDisabled={!tokenContext.isProjectDeveloper(Number(projectId))}
|
||||||
steps={testCase.Steps}
|
steps={testCase.Steps}
|
||||||
onStepUpdate={(stepId, changeStep) => {
|
onStepUpdate={(stepId, changeStep) => {
|
||||||
if (testCase.Steps) {
|
if (testCase.Steps) {
|
||||||
@@ -425,7 +425,7 @@ export default function CaseEditor({
|
|||||||
<h6 className="font-bold">{messages.attachments}</h6>
|
<h6 className="font-bold">{messages.attachments}</h6>
|
||||||
{testCase.Attachments && (
|
{testCase.Attachments && (
|
||||||
<CaseAttachmentsEditor
|
<CaseAttachmentsEditor
|
||||||
isDisabled={!toakenContext.isProjectDeveloper(Number(projectId))}
|
isDisabled={!tokenContext.isProjectDeveloper(Number(projectId))}
|
||||||
attachments={testCase.Attachments}
|
attachments={testCase.Attachments}
|
||||||
onAttachmentDownload={(attachmentId: number, downloadFileName: string) =>
|
onAttachmentDownload={(attachmentId: number, downloadFileName: string) =>
|
||||||
fetchDownloadAttachment(attachmentId, downloadFileName)
|
fetchDownloadAttachment(attachmentId, downloadFileName)
|
||||||
|
|||||||
@@ -76,6 +76,13 @@ export type AdminMessages = {
|
|||||||
noUsersFound: string;
|
noUsersFound: string;
|
||||||
administrator: string;
|
administrator: string;
|
||||||
user: string;
|
user: string;
|
||||||
|
quitAdmin: string;
|
||||||
|
quit: string;
|
||||||
|
quitConfirm: string;
|
||||||
|
close: string;
|
||||||
|
roleChanged: string;
|
||||||
|
lostAdminAuth: string;
|
||||||
|
atLeast: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AccountDropDownMessages = {
|
export type AccountDropDownMessages = {
|
||||||
|
|||||||
@@ -47,4 +47,32 @@ async function searchUsers(jwt: string, projectId: number, searchText: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { findUser, searchUsers };
|
async function updateUserRole(jwt: string, userId: number, newRole: number) {
|
||||||
|
const updateUserData = {
|
||||||
|
newRole,
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchOptions = {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${jwt}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(updateUserData),
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = `${apiServer}/users/${userId}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, fetchOptions);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error fetching data:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { findUser, searchUsers, updateUserRole };
|
||||||
|
|||||||
2374
package-lock.json
generated
2374
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,11 @@
|
|||||||
"@playwright/test": "^1.47.2",
|
"@playwright/test": "^1.47.2",
|
||||||
"@types/node": "^22.5.5",
|
"@types/node": "^22.5.5",
|
||||||
"@vitest/coverage-v8": "^1.6.0",
|
"@vitest/coverage-v8": "^1.6.0",
|
||||||
|
"express": "^4.21.0",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
|
"sequelize": "^6.37.3",
|
||||||
|
"sqlite3": "^5.1.7",
|
||||||
|
"supertest": "^7.0.0",
|
||||||
"vitest": "^1.6.0"
|
"vitest": "^1.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user