mirror of
https://github.com/diced/zipline.git
synced 2026-07-08 12:47:11 -07:00
Merge commit from fork
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { Group, Paper, Stack, Text, Title } from '@mantine/core';
|
||||
import { useUserStore } from '@/lib/client/store/user';
|
||||
import ClearTempButton from './actions/ClearTempButton';
|
||||
import ClearZerosButton from './actions/ClearZerosButton';
|
||||
import GenThumbsButton from './actions/GenThumbsButton';
|
||||
@@ -10,6 +11,7 @@ const ACTIONS = [
|
||||
name: 'Import/Export Data',
|
||||
desc: 'Allows you to import or export server data and configurations.',
|
||||
Component: ImportExport,
|
||||
superAdminOnly: true,
|
||||
},
|
||||
{
|
||||
name: 'Clear Temporary Files',
|
||||
@@ -34,6 +36,9 @@ const ACTIONS = [
|
||||
];
|
||||
|
||||
export default function DashboardServerActions() {
|
||||
const user = useUserStore((state) => state.user);
|
||||
const actions = ACTIONS.filter((action) => !action.superAdminOnly || user?.role === 'SUPERADMIN');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Group gap='sm'>
|
||||
@@ -43,7 +48,7 @@ export default function DashboardServerActions() {
|
||||
Useful tools and scripts for server management.
|
||||
</Text>
|
||||
<Stack gap='xs' my='sm'>
|
||||
{ACTIONS.map(({ name, desc, Component }) => (
|
||||
{actions.map(({ name, desc, Component }) => (
|
||||
<Paper withBorder p='sm' key={name}>
|
||||
<Group gap='md'>
|
||||
<Component />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Response } from '@/lib/api/response';
|
||||
import { readToDataURL } from '@/lib/base64';
|
||||
import { bytes } from '@/lib/bytes';
|
||||
import { User } from '@/lib/db/models/user';
|
||||
import { LimitedUser } from '@/lib/db/models/user';
|
||||
import { fetchApi } from '@/lib/fetchApi';
|
||||
import { canInteract } from '@/lib/role';
|
||||
import { useUserStore } from '@/lib/client/store/user';
|
||||
@@ -31,7 +31,7 @@ export default function EditUserModal({
|
||||
opened,
|
||||
onClose,
|
||||
}: {
|
||||
user?: User | null;
|
||||
user?: LimitedUser | null;
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { User } from '@/lib/db/models/user';
|
||||
import { LimitedUser } from '@/lib/db/models/user';
|
||||
import { ActionIcon, Avatar, Card, Group, Menu, Stack, Text } from '@mantine/core';
|
||||
import { useUserStore } from '@/lib/client/store/user';
|
||||
import { IconDots, IconFiles, IconTrashFilled, IconUserEdit } from '@tabler/icons-react';
|
||||
@@ -9,7 +9,7 @@ import RelativeDate from '@/components/RelativeDate';
|
||||
import { canInteract, isAdministrator, roleName } from '@/lib/role';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export default function UserCard({ user }: { user: User }) {
|
||||
export default function UserCard({ user }: { user: LimitedUser }) {
|
||||
const currentUser = useUserStore((state) => state.user);
|
||||
|
||||
const [opened, setOpen] = useState(false);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Response } from '@/lib/api/response';
|
||||
import { User } from '@/lib/db/models/user';
|
||||
import { LimitedUser } from '@/lib/db/models/user';
|
||||
import { fetchApi } from '@/lib/fetchApi';
|
||||
import { modals } from '@mantine/modals';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { IconUserCancel, IconUserMinus } from '@tabler/icons-react';
|
||||
import { mutate } from 'swr';
|
||||
|
||||
export async function deleteUser(user: User) {
|
||||
export async function deleteUser(user: LimitedUser) {
|
||||
modals.openConfirmModal({
|
||||
centered: true,
|
||||
title: `Delete ${user.username}?`,
|
||||
@@ -33,7 +33,7 @@ export async function deleteUser(user: User) {
|
||||
});
|
||||
}
|
||||
|
||||
async function handleDeleteUser(user: User, deleteFiles: boolean = false) {
|
||||
async function handleDeleteUser(user: LimitedUser, deleteFiles: boolean = false) {
|
||||
const { data, error } = await fetchApi<Response['/api/users/[id]']>(`/api/users/${user.id}`, 'DELETE', {
|
||||
delete: deleteFiles,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import GridTableSwitcher from '@/components/GridTableSwitcher';
|
||||
import { Response } from '@/lib/api/response';
|
||||
import { readToDataURL } from '@/lib/base64';
|
||||
import { User } from '@/lib/db/models/user';
|
||||
import { LimitedUser } from '@/lib/db/models/user';
|
||||
import { fetchApi } from '@/lib/fetchApi';
|
||||
import { canInteract } from '@/lib/role';
|
||||
import { useUserStore } from '@/lib/client/store/user';
|
||||
@@ -68,7 +67,7 @@ export default function DashboardUsers() {
|
||||
}
|
||||
}
|
||||
|
||||
const { data, error } = await fetchApi<Extract<Response['/api/users'], User>>('/api/users', 'POST', {
|
||||
const { data, error } = await fetchApi<LimitedUser>('/api/users', 'POST', {
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
role: values.role ?? 'USER',
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { Response } from '@/lib/api/response';
|
||||
import { User } from '@/lib/db/models/user';
|
||||
import { LimitedUser } from '@/lib/db/models/user';
|
||||
import { Center, Group, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import { IconFilesOff } from '@tabler/icons-react';
|
||||
import useSWR from 'swr';
|
||||
import UserCard from '../UserCard';
|
||||
|
||||
export default function UserGridView() {
|
||||
const { data: users, isLoading } =
|
||||
useSWR<Extract<Response['/api/users'], User[]>>('/api/users?noincl=true');
|
||||
const { data: users, isLoading } = useSWR<LimitedUser[]>('/api/users?noincl=true');
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import RelativeDate from '@/components/RelativeDate';
|
||||
import { Response } from '@/lib/api/response';
|
||||
import { useUserStore } from '@/lib/client/store/user';
|
||||
import { User } from '@/lib/db/models/user';
|
||||
import { LimitedUser } from '@/lib/db/models/user';
|
||||
import { canInteract, roleName } from '@/lib/role';
|
||||
import { ActionIcon, Avatar, Box, Group, Tooltip } from '@mantine/core';
|
||||
import { IconEdit, IconFiles, IconTrashFilled } from '@tabler/icons-react';
|
||||
@@ -15,20 +14,20 @@ import EditUserModal from '../EditUserModal';
|
||||
export default function UserTableView() {
|
||||
const currentUser = useUserStore((state) => state.user);
|
||||
|
||||
const { data, isLoading } = useSWR<Extract<Response['/api/users'], User[]>>('/api/users?noincl=true');
|
||||
const { data, isLoading } = useSWR<LimitedUser[]>('/api/users?noincl=true');
|
||||
|
||||
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
||||
const [selectedUser, setSelectedUser] = useState<LimitedUser | null>(null);
|
||||
|
||||
const [sortStatus, setSortStatus] = useState<DataTableSortStatus>({
|
||||
columnAccessor: 'createdAt',
|
||||
direction: 'desc',
|
||||
});
|
||||
|
||||
const sorted = useMemo<User[]>(() => {
|
||||
const sorted = useMemo<LimitedUser[]>(() => {
|
||||
if (!data) return [];
|
||||
|
||||
const { columnAccessor, direction } = sortStatus;
|
||||
const key = columnAccessor as keyof User;
|
||||
const key = columnAccessor as keyof LimitedUser;
|
||||
|
||||
return [...data].sort((a, b) => {
|
||||
const av = a[key]!;
|
||||
|
||||
@@ -92,6 +92,7 @@ export const API_ERRORS = {
|
||||
3016: 'OAuth registration is disabled',
|
||||
3017: 'OAuth login is not allowed for this account',
|
||||
3018: 'Invalid access token provided.',
|
||||
3019: 'You cannot modify this user',
|
||||
|
||||
// 4xxx, not founds
|
||||
4000: 'File not found',
|
||||
|
||||
@@ -14,6 +14,16 @@ export const userSelect = {
|
||||
sessions: true,
|
||||
};
|
||||
|
||||
export const limitedUserSelect = {
|
||||
id: true,
|
||||
username: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
role: true,
|
||||
view: true,
|
||||
quota: true,
|
||||
};
|
||||
|
||||
export const userViewSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().nullish(),
|
||||
@@ -106,3 +116,14 @@ export const userSchema = z.object({
|
||||
});
|
||||
|
||||
export type User = z.infer<typeof userSchema>;
|
||||
|
||||
export const limitedUserSchema = userSchema.omit({
|
||||
oauthProviders: true,
|
||||
totpSecret: true,
|
||||
passkeys: true,
|
||||
sessions: true,
|
||||
password: true,
|
||||
token: true,
|
||||
});
|
||||
|
||||
export type LimitedUser = z.infer<typeof limitedUserSchema>;
|
||||
|
||||
@@ -11,6 +11,12 @@ export function canInteract(current?: Role, target?: Role) {
|
||||
);
|
||||
}
|
||||
|
||||
export function interactableRoles(current?: Role): Role[] {
|
||||
if (current === 'SUPERADMIN') return ['USER', 'ADMIN'];
|
||||
if (current === 'ADMIN') return ['USER'];
|
||||
return [];
|
||||
}
|
||||
|
||||
export function roleName(role?: Role) {
|
||||
switch (role) {
|
||||
case 'USER':
|
||||
|
||||
@@ -70,6 +70,8 @@ export default typedPlugin(
|
||||
preHandler: [userMiddleware, administratorMiddleware],
|
||||
},
|
||||
async (req, res) => {
|
||||
if (req.user.role !== 'SUPERADMIN') throw new ApiError(3015);
|
||||
|
||||
if (req.query.counts) {
|
||||
const counts = await getCounts();
|
||||
|
||||
|
||||
@@ -37,24 +37,23 @@ export default typedPlugin(
|
||||
preHandler: [userMiddleware],
|
||||
},
|
||||
async (req, res) => {
|
||||
const { noincl, user, parentId, root } = req.query;
|
||||
const { noincl, user: userId, parentId, root } = req.query;
|
||||
|
||||
if (user) {
|
||||
const user = await prisma.user.findUnique({
|
||||
if (userId) {
|
||||
const targetUser = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: req.user.id,
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) throw new ApiError(4009);
|
||||
if (req.user.id !== user.id) {
|
||||
if (!canInteract(req.user.role, user.role)) throw new ApiError(4009);
|
||||
}
|
||||
if (!targetUser) throw new ApiError(4009);
|
||||
if (req.user.id !== targetUser.id && !canInteract(req.user.role, targetUser.role))
|
||||
throw new ApiError(4009);
|
||||
}
|
||||
|
||||
const folders = await prisma.folder.findMany({
|
||||
where: {
|
||||
userId: user || req.user.id,
|
||||
userId: userId || req.user.id,
|
||||
...(root && { parentId: null }),
|
||||
...(parentId && { parentId }),
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ import { bytes } from '@/lib/bytes';
|
||||
import { hashPassword } from '@/lib/crypto';
|
||||
import { datasource } from '@/lib/datasource';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { User, userSchema, userSelect } from '@/lib/db/models/user';
|
||||
import { LimitedUser, limitedUserSchema, limitedUserSelect } from '@/lib/db/models/user';
|
||||
import { log } from '@/lib/logger';
|
||||
import { canInteract } from '@/lib/role';
|
||||
import { zStringTrimmed } from '@/lib/validation';
|
||||
@@ -13,7 +13,7 @@ import { userMiddleware } from '@/server/middleware/user';
|
||||
import typedPlugin from '@/server/typedPlugin';
|
||||
import { z } from 'zod';
|
||||
|
||||
export type ApiUsersIdResponse = User;
|
||||
export type ApiUsersIdResponse = LimitedUser;
|
||||
|
||||
const logger = log('api').c('users').c('[id]');
|
||||
|
||||
@@ -31,7 +31,7 @@ export default typedPlugin(
|
||||
description: 'Fetch a specific user by ID, including their profile and role (admin only).',
|
||||
params: paramsSchema,
|
||||
response: {
|
||||
200: userSchema,
|
||||
200: limitedUserSchema,
|
||||
},
|
||||
tags: ['auth', 'admin'],
|
||||
},
|
||||
@@ -42,10 +42,11 @@ export default typedPlugin(
|
||||
where: {
|
||||
id: req.params.id,
|
||||
},
|
||||
select: userSelect,
|
||||
select: limitedUserSelect,
|
||||
});
|
||||
|
||||
if (!user) throw new ApiError(4009);
|
||||
if (!canInteract(req.user.role, user.role)) throw new ApiError(4009);
|
||||
|
||||
return res.send(user);
|
||||
},
|
||||
@@ -73,7 +74,7 @@ export default typedPlugin(
|
||||
.optional(),
|
||||
}),
|
||||
response: {
|
||||
200: userSchema,
|
||||
200: limitedUserSchema,
|
||||
},
|
||||
tags: ['auth', 'admin'],
|
||||
},
|
||||
@@ -84,9 +85,13 @@ export default typedPlugin(
|
||||
where: {
|
||||
id: req.params.id,
|
||||
},
|
||||
select: userSelect,
|
||||
select: {
|
||||
id: true,
|
||||
role: true,
|
||||
},
|
||||
});
|
||||
if (!user) throw new ApiError(4009);
|
||||
if (!canInteract(req.user.role, user.role)) throw new ApiError(3019);
|
||||
|
||||
const { username, password, avatar, role, quota } = req.body;
|
||||
if (role && !canInteract(req.user.role, role)) throw new ApiError(3007);
|
||||
@@ -149,11 +154,7 @@ export default typedPlugin(
|
||||
},
|
||||
}),
|
||||
},
|
||||
select: {
|
||||
...userSelect,
|
||||
totpSecret: false,
|
||||
passkeys: false,
|
||||
},
|
||||
select: limitedUserSelect,
|
||||
});
|
||||
|
||||
logger.info(`${req.user.username} updated another user`, {
|
||||
@@ -176,7 +177,7 @@ export default typedPlugin(
|
||||
delete: z.boolean().optional(),
|
||||
}),
|
||||
response: {
|
||||
200: userSchema,
|
||||
200: limitedUserSchema,
|
||||
},
|
||||
tags: ['auth', 'admin'],
|
||||
},
|
||||
@@ -187,7 +188,11 @@ export default typedPlugin(
|
||||
where: {
|
||||
id: req.params.id,
|
||||
},
|
||||
select: userSelect,
|
||||
select: {
|
||||
id: true,
|
||||
role: true,
|
||||
username: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) throw new ApiError(4009);
|
||||
@@ -242,10 +247,7 @@ export default typedPlugin(
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
select: {
|
||||
...userSelect,
|
||||
totpSecret: false,
|
||||
},
|
||||
select: limitedUserSelect,
|
||||
});
|
||||
|
||||
logger.info(`${req.user.username} deleted another user`, {
|
||||
|
||||
@@ -2,10 +2,10 @@ import { ApiError } from '@/lib/api/errors';
|
||||
import { config } from '@/lib/config';
|
||||
import { createToken, hashPassword } from '@/lib/crypto';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { User, userSchema, userSelect } from '@/lib/db/models/user';
|
||||
import { LimitedUser, limitedUserSchema, limitedUserSelect } from '@/lib/db/models/user';
|
||||
import { log } from '@/lib/logger';
|
||||
import { secondlyRatelimit } from '@/lib/ratelimits';
|
||||
import { canInteract } from '@/lib/role';
|
||||
import { canInteract, interactableRoles } from '@/lib/role';
|
||||
import { zQsBoolean, zStringTrimmed } from '@/lib/validation';
|
||||
import { Role } from '@/prisma/client';
|
||||
import { administratorMiddleware } from '@/server/middleware/administrator';
|
||||
@@ -14,7 +14,7 @@ import typedPlugin from '@/server/typedPlugin';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { z } from 'zod';
|
||||
|
||||
export type ApiUsersResponse = User[] | User;
|
||||
export type ApiUsersResponse = LimitedUser[] | LimitedUser;
|
||||
|
||||
const logger = log('api').c('users');
|
||||
|
||||
@@ -33,19 +33,22 @@ export default typedPlugin(
|
||||
'List users in the instance, optionally excluding the current admin from the results (admin only).',
|
||||
querystring: querySchema,
|
||||
response: {
|
||||
200: z.array(userSchema),
|
||||
200: z.array(limitedUserSchema),
|
||||
},
|
||||
tags: ['auth', 'admin'],
|
||||
},
|
||||
preHandler: [userMiddleware, administratorMiddleware],
|
||||
},
|
||||
async (req, res) => {
|
||||
const roles = interactableRoles(req.user.role);
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
select: {
|
||||
...userSelect,
|
||||
...limitedUserSelect,
|
||||
avatar: true,
|
||||
},
|
||||
where: {
|
||||
role: { in: roles },
|
||||
...(req.query.noincl && { id: { not: req.user.id } }),
|
||||
},
|
||||
});
|
||||
@@ -67,7 +70,7 @@ export default typedPlugin(
|
||||
role: z.enum(Role).default('USER').optional(),
|
||||
}),
|
||||
response: {
|
||||
200: userSchema,
|
||||
200: limitedUserSchema,
|
||||
},
|
||||
tags: ['auth', 'admin'],
|
||||
},
|
||||
@@ -106,11 +109,7 @@ export default typedPlugin(
|
||||
avatar: avatar64 ?? null,
|
||||
token: createToken(),
|
||||
},
|
||||
select: {
|
||||
...userSelect,
|
||||
totpSecret: false,
|
||||
passkeys: false,
|
||||
},
|
||||
select: limitedUserSelect,
|
||||
});
|
||||
|
||||
logger.info(`${req.user.username} created a new user`, {
|
||||
|
||||
Reference in New Issue
Block a user