diff --git a/app/db/models.py b/app/db/models.py index b966739..2b05227 100644 --- a/app/db/models.py +++ b/app/db/models.py @@ -270,6 +270,10 @@ class User(Base): + f"/sub/{self.username}/{self.key}" ) + @hybrid_property + def owner_username(self): + return self.admin.username + class Inbound(Base): __tablename__ = "inbounds" diff --git a/app/models/user.py b/app/models/user.py index de56730..77d65ae 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -126,6 +126,7 @@ class UserResponse(User): created_at: datetime service_ids: list[int] subscription_url: str + owner_username: str model_config = ConfigDict(from_attributes=True) diff --git a/app/routes/user.py b/app/routes/user.py index 707e13c..e788d79 100644 --- a/app/routes/user.py +++ b/app/routes/user.py @@ -49,6 +49,7 @@ user_filters = [ "expired", "data_limit_reached", "enabled", + "owner_username", ] @@ -64,6 +65,7 @@ def get_users( expired: bool | None = Query(None), data_limit_reached: bool | None = Query(None), enabled: bool | None = Query(None), + owner_username: str | None = Query(None), ): """ Filters users based on the options @@ -84,6 +86,13 @@ def get_users( query = query.filter( User.username.ilike(f"%{username[0]}%") ) + elif name == "owner_username": + if not dbadmin.is_sudo: + raise HTTPException(403, "You're not allowed.") + filter_admin = crud.get_admin(db, value) + if not filter_admin: + raise HTTPException(404, "owner_username not found.") + query = query.filter(User.admin_id == filter_admin.id) else: query = query.filter(getattr(User, name) == value) diff --git a/dashboard/public/locales/en.json b/dashboard/public/locales/en.json index e8e1595..262c627 100644 --- a/dashboard/public/locales/en.json +++ b/dashboard/public/locales/en.json @@ -71,6 +71,7 @@ "admins": "Admins", "normal": "Normal", "sudo": "Sudo", + "owner": "Owner", "support-us": { "title": "Support Us", "desc": "Marzneshin is free and open-source software (FOSS), join us and show your support by donating.", diff --git a/dashboard/public/locales/fa.json b/dashboard/public/locales/fa.json index 234952a..22b2ef6 100644 --- a/dashboard/public/locales/fa.json +++ b/dashboard/public/locales/fa.json @@ -67,7 +67,8 @@ "change": "تغییر", "admins": "ادمین‌ها", "normal": "عادی", - "sudo": "سوپر ادمین", + "sudo": "ادمین سودو", + "owner": "مالک", "security": "امنیت", "user_info": "اطلاعات کاربر", "subscription": "اشتراک", diff --git a/dashboard/src/features/admins/components/tables/admins/index.tsx b/dashboard/src/features/admins/components/tables/admins/index.tsx index d6085cd..a5650f3 100644 --- a/dashboard/src/features/admins/components/tables/admins/index.tsx +++ b/dashboard/src/features/admins/components/tables/admins/index.tsx @@ -1,37 +1,43 @@ import type { FC } from "react"; -import { fetchAdmins, AdminsQueryFetchKey } from "@marzneshin/features/admins"; -import { columns } from "./columns"; +import { fetchAdmins, AdminType, AdminsQueryFetchKey } from "@marzneshin/features/admins"; +import { columns as columnsFn } from "./columns"; import { EntityTable } from "@marzneshin/features/entity-table"; import { useNavigate } from "@tanstack/react-router"; export const AdminsTable: FC = () => { const navigate = useNavigate({ from: "/admins" }); + const onOpen = (entity: AdminType) => { + navigate({ + to: "/admins/$adminId", + params: { adminId: entity.username }, + }) + } + + const onEdit = (entity: AdminType) => { + navigate({ + to: "/admins/$adminId/edit", + params: { adminId: entity.username }, + }) + } + + const onDelete = (entity: AdminType) => { + navigate({ + to: "/admins/$adminId/delete", + params: { adminId: entity.username }, + }) + } + + const columns = columnsFn({ onEdit, onDelete, onOpen }); + return ( navigate({ to: "/admins/create" })} - onOpen={(entity) => - navigate({ - to: "/admins/$adminId", - params: { adminId: entity.username }, - }) - } - onEdit={(entity) => - navigate({ - to: "/admins/$adminId/edit", - params: { adminId: entity.username }, - }) - } - onDelete={(entity) => - navigate({ - to: "/admins/$adminId/delete", - params: { adminId: entity.username }, - }) - } + onOpen={onOpen} /> ); }; diff --git a/dashboard/src/features/entity-table/entity-table.tsx b/dashboard/src/features/entity-table/entity-table.tsx index 48a15bc..97290e2 100644 --- a/dashboard/src/features/entity-table/entity-table.tsx +++ b/dashboard/src/features/entity-table/entity-table.tsx @@ -2,6 +2,7 @@ import { useMemo } from "react"; import { Button, DataTableViewOptions } from "@marzneshin/components"; import { useTranslation } from "react-i18next"; import { useQuery } from "@tanstack/react-query"; +import { ColumnDef } from "@tanstack/react-table"; import { EntityTableContext } from "./contexts"; import { TableFiltering, DataTablePagination, EntityDataTable } from "./components"; import { @@ -19,28 +20,24 @@ import { export interface EntityTableProps { fetchEntity: ({ queryKey }: EntityQueryKeyType) => FetchEntityReturn; - columnsFn: any; + columns: ColumnDef[]; filteredColumn: string; entityKey: string; rowSelection?: UseRowSelectionReturn; manualSorting?: boolean; onCreate: () => void; - onEdit: (entity: T) => void; - onOpen: (entity: T) => void; - onDelete: (entity: T) => void; + onOpen: (entity: any) => void; } export function EntityTable({ fetchEntity, - columnsFn, + columns, filteredColumn, rowSelection, entityKey, manualSorting = false, onCreate, - onEdit, onOpen, - onDelete, }: EntityTableProps) { const { t } = useTranslation(); const filtering = useFiltering({ column: filteredColumn }); @@ -68,7 +65,6 @@ export function EntityTable({ initialData: { entity: [], pageCount: 1 }, }); - const columns = columnsFn({ onEdit, onDelete, onOpen }); const table = useEntityTable({ data, columns, diff --git a/dashboard/src/features/entity-table/stories/entity-table.stories.tsx b/dashboard/src/features/entity-table/stories/entity-table.stories.tsx index 225e482..a7acccd 100644 --- a/dashboard/src/features/entity-table/stories/entity-table.stories.tsx +++ b/dashboard/src/features/entity-table/stories/entity-table.stories.tsx @@ -38,28 +38,28 @@ export default meta; type Story = StoryObj; +const onEdit = (entity: any) => alert(`Edit entity: ${JSON.stringify(entity)}`) +const onDelete = (entity: any) => alert(`Delete entity: ${JSON.stringify(entity)}`) +const onOpen = (entity: any) => alert(`Open entity: ${JSON.stringify(entity)}`) + export const BasicEntityTable: Story = { args: { fetchEntity: fetchEntity, - columnsFn: columnsFnBasicEntityTable, + columns: columnsFnBasicEntityTable({ onEdit, onDelete, onOpen }), filteredColumn: "name", entityKey: "entities", onCreate: () => alert("Create entity"), - onEdit: (entity: any) => alert(`Edit entity: ${JSON.stringify(entity)}`), - onOpen: (entity: any) => alert(`Open entity: ${JSON.stringify(entity)}`), - onDelete: (entity: any) => alert(`Delete entity: ${JSON.stringify(entity)}`), + onOpen: onOpen, } }; export const LoadingEntityTable: Story = { args: { fetchEntity: fetchEntityLoading, - columnsFn: columnsFnBasicEntityTable, + columns: columnsFnBasicEntityTable({ onEdit, onDelete, onOpen }), filteredColumn: "name", entityKey: "entities", onCreate: () => alert("Create entity"), - onEdit: (entity: any) => alert(`Edit entity: ${JSON.stringify(entity)}`), - onOpen: (entity: any) => alert(`Open entity: ${JSON.stringify(entity)}`), - onDelete: (entity: any) => alert(`Delete entity: ${JSON.stringify(entity)}`), + onOpen: onOpen, } }; diff --git a/dashboard/src/features/entity-table/types/columns-sudo-visibility.ts b/dashboard/src/features/entity-table/types/columns-sudo-visibility.ts new file mode 100644 index 0000000..cbc4ce1 --- /dev/null +++ b/dashboard/src/features/entity-table/types/columns-sudo-visibility.ts @@ -0,0 +1,5 @@ +import type { ColumnDef } from "@tanstack/react-table"; + +export type ColumnDefWithSudoRole = ColumnDef & { + sudoVisibleOnly?: boolean, +}; diff --git a/dashboard/src/features/entity-table/types/index.ts b/dashboard/src/features/entity-table/types/index.ts index 31012e2..3cd6f35 100644 --- a/dashboard/src/features/entity-table/types/index.ts +++ b/dashboard/src/features/entity-table/types/index.ts @@ -1 +1,2 @@ export * from "./column-actions" +export * from "./columns-sudo-visibility" diff --git a/dashboard/src/features/nodes/dialogs/mutation/index.tsx b/dashboard/src/features/nodes/dialogs/mutation/index.tsx index 6f2935e..95ffc0e 100644 --- a/dashboard/src/features/nodes/dialogs/mutation/index.tsx +++ b/dashboard/src/features/nodes/dialogs/mutation/index.tsx @@ -38,6 +38,7 @@ export const MutationDialog: FC> = ({ const { t } = useTranslation(); const defaultValue = useMemo(() => ({ + id: 0, name: "", address: "", status: "none", diff --git a/dashboard/src/features/nodes/tables/table.tsx b/dashboard/src/features/nodes/tables/table.tsx index 0d82dcc..fb620c8 100644 --- a/dashboard/src/features/nodes/tables/table.tsx +++ b/dashboard/src/features/nodes/tables/table.tsx @@ -1,38 +1,46 @@ import { FC } from "react"; import { - columns, + columns as columnsFn, fetchNodes, + NodeType } from '@marzneshin/features/nodes'; import { EntityTable } from "@marzneshin/features/entity-table"; import { useNavigate } from "@tanstack/react-router"; export const NodesTable: FC = () => { const navigate = useNavigate({ from: "/nodes" }); + + const onOpen = (entity: NodeType) => { + navigate({ + to: "/nodes/$nodeId", + params: { nodeId: String(entity.id) }, + }) + } + + const onEdit = (entity: NodeType) => { + navigate({ + to: "/nodes/$nodeId/edit", + params: { nodeId: String(entity.id) }, + }) + } + + const onDelete = (entity: NodeType) => { + navigate({ + to: "/nodes/$nodeId/delete", + params: { nodeId: String(entity.id) }, + }) + } + + const columns = columnsFn({ onEdit, onDelete, onOpen }); + return ( navigate({ to: "/nodes/create" })} - onOpen={(entity) => - navigate({ - to: "/nodes/$nodeId", - params: { nodeId: String(entity.id) }, - }) - } - onEdit={(entity) => - navigate({ - to: "/nodes/$nodeId/edit", - params: { nodeId: String(entity.id) }, - }) - } - onDelete={(entity) => - navigate({ - to: "/nodes/$nodeId/delete", - params: { nodeId: String(entity.id) }, - }) - } + onOpen={onOpen} /> ) } diff --git a/dashboard/src/features/nodes/types/index.ts b/dashboard/src/features/nodes/types/index.ts index f1c4256..ff65f64 100644 --- a/dashboard/src/features/nodes/types/index.ts +++ b/dashboard/src/features/nodes/types/index.ts @@ -3,43 +3,43 @@ import { PowerOff, Zap, ZapOff } from "lucide-react"; import { z } from "zod"; export const NodesStatus = { - healthy: { - label: "healthy", - icon: Zap, - }, - unhealthy: { - label: "unhealthy", - icon: ZapOff, - }, - disabled: { - label: "disabled", - icon: PowerOff, - }, - none: { - label: "none", - icon: null, - }, + healthy: { + label: "healthy", + icon: Zap, + }, + unhealthy: { + label: "unhealthy", + icon: ZapOff, + }, + disabled: { + label: "disabled", + icon: PowerOff, + }, + none: { + label: "none", + icon: null, + }, } as Record; export const NodeSchema = z.object({ - name: z.string().min(1), - address: z.string().min(1), - port: z - .number() - .min(1) - .or(z.string().transform((v) => Number.parseFloat(v))), - id: z.number().nullable().optional(), - status: z.enum([ - NodesStatus.healthy.label, - NodesStatus.unhealthy.label, - "none", - NodesStatus.disabled.label, - ]), - usage_coefficient: z - .number() - .default(1.0) - .or(z.string().transform((v) => Number.parseFloat(v))), - connection_backend: z.enum(["grpclib", "grpcio"]).default("grpclib"), + name: z.string().min(1), + address: z.string().min(1), + port: z + .number() + .min(1) + .or(z.string().transform((v) => Number.parseFloat(v))), + id: z.number().nullable().optional(), + status: z.enum([ + NodesStatus.healthy.label, + NodesStatus.unhealthy.label, + "none", + NodesStatus.disabled.label, + ]), + usage_coefficient: z + .number() + .default(1.0) + .or(z.string().transform((v) => Number.parseFloat(v))), + connection_backend: z.enum(["grpclib", "grpcio"]).default("grpclib"), }); -export type NodeType = z.infer; +export type NodeType = z.infer & { id: number }; diff --git a/dashboard/src/features/services/components/tables/services/index.tsx b/dashboard/src/features/services/components/tables/services/index.tsx index 14a4b5e..f3c9b4e 100644 --- a/dashboard/src/features/services/components/tables/services/index.tsx +++ b/dashboard/src/features/services/components/tables/services/index.tsx @@ -1,40 +1,46 @@ import type { FC } from "react"; import { - ServicesQueryFetchKey, - fetchServices, - ServiceType + ServicesQueryFetchKey, + fetchServices, + ServiceType } from "@marzneshin/features/services"; -import { columns } from "./columns"; +import { columns as columnsFn } from "./columns"; import { EntityTable } from "@marzneshin/features/entity-table"; import { useNavigate } from "@tanstack/react-router"; export const ServicesTable: FC = () => { - const navigate = useNavigate({ from: "/services" }); - return ( - navigate({ to: "/services/create" })} - onOpen={(entity: ServiceType) => - navigate({ - to: "/services/$serviceId", - params: { serviceId: String(entity.id) }, - }) + const navigate = useNavigate({ from: "/services" }); + const onEdit = (entity: ServiceType) => { + navigate({ + to: "/services/$serviceId/edit", + params: { serviceId: String(entity.id) }, + }) } - onEdit={(entity: ServiceType) => - navigate({ - to: "/services/$serviceId/edit", - params: { serviceId: String(entity.id) }, - }) + + const onDelete = (entity: ServiceType) => { + navigate({ + to: "/services/$serviceId/delete", + params: { serviceId: String(entity.id) }, + }) } - onDelete={(entity: ServiceType) => - navigate({ - to: "/services/$serviceId/delete", - params: { serviceId: String(entity.id) }, - }) + + const onOpen = (entity: ServiceType) => { + navigate({ + to: "/services/$serviceId", + params: { serviceId: String(entity.id) }, + }) } - /> - ); + + const columns = columnsFn({ onEdit, onDelete, onOpen }); + + return ( + navigate({ to: "/services/create" })} + onOpen={onOpen} + /> + ); }; diff --git a/dashboard/src/features/users/components/tables/users/columns.tsx b/dashboard/src/features/users/components/tables/users/columns.tsx index 4052e68..4c387f9 100644 --- a/dashboard/src/features/users/components/tables/users/columns.tsx +++ b/dashboard/src/features/users/components/tables/users/columns.tsx @@ -1,4 +1,3 @@ -import type { ColumnDef } from "@tanstack/react-table"; import { type UserType, OnlineStatus, @@ -17,9 +16,9 @@ import { } from "@marzneshin/components"; import { LinkIcon } from "lucide-react"; import { getSubscriptionLink } from "@marzneshin/utils"; -import { type ColumnActions } from "@marzneshin/features/entity-table"; +import type { ColumnActions, ColumnDefWithSudoRole } from "@marzneshin/features/entity-table"; -export const columns = (actions: ColumnActions): ColumnDef[] => [ +export const columns = (actions: ColumnActions): ColumnDefWithSudoRole[] => [ { accessorKey: "username", header: ({ column }) => ( @@ -30,7 +29,7 @@ export const columns = (actions: ColumnActions): ColumnDef[] {row.original.username} ), - }, + }, { accessorKey: "activated", enableSorting: false, @@ -42,6 +41,17 @@ export const columns = (actions: ColumnActions): ColumnDef[] ), cell: ({ row }) => , }, + { + accessorKey: "owner_username", + enableSorting: false, + sudoVisibleOnly: true, + header: ({ column }) => ( + + ), + }, { accessorKey: "used_traffic", header: ({ column }) => ( diff --git a/dashboard/src/features/users/components/tables/users/index.tsx b/dashboard/src/features/users/components/tables/users/index.tsx index f3c9fab..badeacb 100644 --- a/dashboard/src/features/users/components/tables/users/index.tsx +++ b/dashboard/src/features/users/components/tables/users/index.tsx @@ -1,37 +1,47 @@ import type { FC } from "react"; -import { fetchUsers, UsersQueryFetchKey } from "@marzneshin/features/users"; -import { columns } from "./columns"; +import { fetchUsers, UserType, UsersQueryFetchKey } from "@marzneshin/features/users"; +import { columns as columnsFn } from "./columns"; import { EntityTable } from "@marzneshin/features/entity-table"; +import { useAuth } from "@marzneshin/features/auth"; import { useNavigate } from "@tanstack/react-router"; export const UsersTable: FC = () => { const navigate = useNavigate({ from: "/users" }); + const { isSudo } = useAuth(); + + const onOpen = (entity: UserType) => { + navigate({ + to: "/users/$userId", + params: { userId: entity.username }, + }) + } + + const onEdit = (entity: UserType) => { + navigate({ + to: "/users/$userId/edit", + params: { userId: entity.username }, + }) + } + + const onDelete = (entity: UserType) => { + navigate({ + to: "/users/$userId/delete", + params: { userId: entity.username }, + }) + } + + const columns = columnsFn({ onEdit, onDelete, onOpen }); + const noneSudoColumns = columns.filter((column) => !column.sudoVisibleOnly); + return ( navigate({ to: "/users/create" })} - onOpen={(entity) => - navigate({ - to: "/users/$userId", - params: { userId: entity.username }, - }) - } - onEdit={(entity) => - navigate({ - to: "/users/$userId/edit", - params: { userId: entity.username }, - }) - } - onDelete={(entity) => - navigate({ - to: "/users/$userId/delete", - params: { userId: entity.username }, - }) - } + onOpen={onOpen} /> ); };