gitscope
← marzneshin / Diff · 9ddfbc8 → 9ddfbc8
Changes 9ddfbc8..9ddfbc8
Patch commit
diff --git a/app/db/crud.py b/app/db/crud.py
index 197684a..52610c1 100644
--- a/app/db/crud.py
+++ b/app/db/crud.py
@@ -314,7 +314,7 @@ def get_users_count(
):
query = db.query(User.id)
if admin:
- query = query.filter(User.admin == admin)
+ query = query.filter(User.admin_id == admin.id)
if status:
query = query.filter(User.status == status)
if enabled:
diff --git a/app/models/admin.py b/app/models/admin.py
index 2f13ddd..998de2f 100644
--- a/app/models/admin.py
+++ b/app/models/admin.py
@@ -12,10 +12,12 @@ oauth2_scheme = OAuth2PasswordBearer(
class Token(BaseModel):
access_token: str
+ is_sudo: bool
token_type: str = "bearer"
class Admin(BaseModel):
+ id: int | None = None
username: str
is_sudo: bool
model_config = ConfigDict(from_attributes=True)
diff --git a/app/routes/admin.py b/app/routes/admin.py
index 4932dd4..b3340a1 100644
--- a/app/routes/admin.py
+++ b/app/routes/admin.py
@@ -62,9 +62,10 @@ def admin_token(
db, form_data.username, form_data.password
):
return Token(
+ is_sudo=dbadmin.is_sudo,
access_token=create_admin_token(
form_data.username, is_sudo=dbadmin.is_sudo
- )
+ ),
)
raise HTTPException(
diff --git a/app/routes/service.py b/app/routes/service.py
index 07ab278..6873ae1 100644
--- a/app/routes/service.py
+++ b/app/routes/service.py
@@ -1,5 +1,5 @@
import sqlalchemy
-from fastapi import APIRouter, Depends, Query
+from fastapi import APIRouter, Query
from fastapi import HTTPException
from fastapi_pagination.ext.sqlalchemy import paginate
from fastapi_pagination.links import Page
@@ -7,18 +7,16 @@ from fastapi_pagination.links import Page
from app import marznode
from app.db import crud
from app.db.models import Service, User
-from app.dependencies import DBDep, sudo_admin
+from app.dependencies import DBDep, AdminDep, SudoAdminDep
from app.models.proxy import Inbound
from app.models.service import ServiceCreate, ServiceModify, ServiceResponse
from app.models.user import UserResponse
-router = APIRouter(
- prefix="/services", dependencies=[Depends(sudo_admin)], tags=["Service"]
-)
+router = APIRouter(prefix="/services", tags=["Service"])
@router.get("", response_model=Page[ServiceResponse])
-def get_services(db: DBDep, name: str = Query(None)):
+def get_services(db: DBDep, admin: AdminDep, name: str = Query(None)):
query = db.query(Service)
if name:
query = query.filter(Service.name.ilike(f"%{name}%"))
@@ -26,7 +24,7 @@ def get_services(db: DBDep, name: str = Query(None)):
@router.post("", response_model=ServiceResponse)
-def add_service(new_service: ServiceCreate, db: DBDep):
+def add_service(new_service: ServiceCreate, db: DBDep, admin: SudoAdminDep):
"""
Add a new service
@@ -43,7 +41,7 @@ def add_service(new_service: ServiceCreate, db: DBDep):
@router.get("/{id}", response_model=ServiceResponse)
-def get_service(id: int, db: DBDep):
+def get_service(id: int, db: DBDep, admin: AdminDep):
"""
Get Service information with id
"""
@@ -55,7 +53,7 @@ def get_service(id: int, db: DBDep):
@router.get("/{id}/users", response_model=Page[UserResponse])
-def get_service_users(id: int, db: DBDep):
+def get_service_users(id: int, db: DBDep, admin: SudoAdminDep):
"""
Get service users
"""
@@ -70,7 +68,7 @@ def get_service_users(id: int, db: DBDep):
@router.get("/{id}/inbounds", response_model=Page[Inbound])
-def get_service_inbounds(id: int, db: DBDep):
+def get_service_inbounds(id: int, db: DBDep, admin: SudoAdminDep):
"""
Get service inbounds
"""
@@ -89,7 +87,9 @@ def get_service_inbounds(id: int, db: DBDep):
@router.put("/{id}", response_model=ServiceResponse)
-async def modify_service(id: int, modification: ServiceModify, db: DBDep):
+async def modify_service(
+ id: int, modification: ServiceModify, db: DBDep, admin: SudoAdminDep
+):
"""
Modify Service
@@ -117,7 +117,7 @@ async def modify_service(id: int, modification: ServiceModify, db: DBDep):
@router.delete("/{id}")
-def remove_service(id: int, db: DBDep):
+def remove_service(id: int, db: DBDep, admin: SudoAdminDep):
dbservice = crud.get_service(db, id)
if not dbservice:
raise HTTPException(status_code=404, detail="Service not found")
diff --git a/dashboard/src/features/auth/auth.store.ts b/dashboard/src/features/auth/auth.store.ts
index 298d22e..23edec7 100644
--- a/dashboard/src/features/auth/auth.store.ts
+++ b/dashboard/src/features/auth/auth.store.ts
@@ -5,15 +5,17 @@ import { subscribeWithSelector } from 'zustand/middleware';
type AdminStateType = {
isLoggedIn: () => Promise<boolean>,
getAuthToken: () => string | null;
+ isSudo: () => boolean;
setAuthToken: (token: string) => void;
removeAuthToken: () => void;
+ setSudo: (isSudo: boolean) => void;
}
export const useAuth = create(
subscribeWithSelector<AdminStateType>(() => ({
isLoggedIn: async () => {
try {
- await fetch('/admins');
+ await fetch('/admins/current');
return true;
} catch (error) {
return false;
@@ -22,6 +24,13 @@ export const useAuth = create(
getAuthToken: () => {
return localStorage.getItem('token');
},
+ isSudo: () => {
+ const isSudo = localStorage.getItem('is-sudo')
+ return isSudo === "true";
+ },
+ setSudo: (isSudo) => {
+ localStorage.setItem('is-sudo', String(isSudo));
+ },
setAuthToken: (token: string) => {
localStorage.setItem('token', token);
},
diff --git a/dashboard/src/features/auth/login/form.tsx b/dashboard/src/features/auth/login/form.tsx
index 8363239..8cfb4e8 100644
--- a/dashboard/src/features/auth/login/form.tsx
+++ b/dashboard/src/features/auth/login/form.tsx
@@ -10,7 +10,7 @@ import { useTranslation } from "react-i18next";
import { FormError } from "./form-error";
export const LoginForm = () => {
- const { setAuthToken } = useAuth();
+ const { setAuthToken, setSudo } = useAuth();
const form = useForm({
resolver: zodResolver(LoginSchema),
defaultValues: {
@@ -22,20 +22,21 @@ export const LoginForm = () => {
const [error, setError] = useState<string>('');
const { t } = useTranslation();
- const submit = (values: FieldValues) => {
+ const submit = async (values: FieldValues) => {
setError('');
const formData = new FormData();
formData.append('username', values.username);
formData.append('password', values.password);
formData.append('grant_type', 'password');
- fetch('/admins/token', { method: 'post', body: formData })
- .then(({ access_token: token }) => {
- setAuthToken(token);
- navigate({ to: '/' });
- })
- .catch((err) => {
- setError(err.response._data?.detail);
- });
+
+ try {
+ const { access_token, is_sudo } = await fetch('/admins/token', { method: 'post', body: formData });
+ setAuthToken(access_token);
+ setSudo(is_sudo);
+ navigate({ to: '/' });
+ } catch (err: any) {
+ setError(err.response._data?.detail || 'An error occurred');
+ }
};
return (
diff --git a/dashboard/src/features/search-command/command-box.tsx b/dashboard/src/features/search-command/command-box.tsx
index b6fc29d..4bf600d 100644
--- a/dashboard/src/features/search-command/command-box.tsx
+++ b/dashboard/src/features/search-command/command-box.tsx
@@ -1,102 +1,113 @@
import {
- UsersIcon,
- ServerIcon,
- BoxIcon,
- ServerCogIcon,
- SettingsIcon,
+ UsersIcon,
+ ServerIcon,
+ BoxIcon,
+ ServerCogIcon,
+ SettingsIcon,
} from "lucide-react";
import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import {
- CommandDialog,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
- CommandSeparator,
+ CommandDialog,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+ CommandSeparator,
} from "@marzneshin/components";
import { useNavigate } from "@tanstack/react-router";
import { SearchBox } from "./search-box";
+import { useAuth } from "@marzneshin/features/auth";
export function CommandBox() {
- const [open, setOpen] = useState(false);
- const { t } = useTranslation();
- const navigate = useNavigate();
+ const [open, setOpen] = useState(false);
+ const { t } = useTranslation();
+ const navigate = useNavigate();
- useEffect(() => {
- const down = (e: KeyboardEvent) => {
- if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
- e.preventDefault();
- setOpen((open) => !open);
- }
- };
- document.addEventListener("keydown", down);
- return () => document.removeEventListener("keydown", down);
- }, []);
+ useEffect(() => {
+ const down = (e: KeyboardEvent) => {
+ if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
+ e.preventDefault();
+ setOpen((open) => !open);
+ }
+ };
+ document.addEventListener("keydown", down);
+ return () => document.removeEventListener("keydown", down);
+ }, []);
- return (
- <>
- <SearchBox onClick={() => setOpen(true)} />
- <CommandDialog open={open} onOpenChange={setOpen}>
- <CommandInput
- placeholder="Type a command or search..."
- className="focus:ring-0 ring-0 m-1 border-none"
- />
- <CommandList>
- <CommandEmpty>No results found.</CommandEmpty>
- <CommandGroup heading="Pages">
- <CommandItem
- onSelect={() => {
- navigate({ to: "/users" });
- setOpen(false);
- }}
- >
- <UsersIcon className="mr-2 size-4" />
- <span>{t("users")}</span>
- </CommandItem>
- <CommandItem
- onSelect={() => {
- navigate({ to: "/services" });
- setOpen(false);
- }}
- >
- <ServerIcon className="mr-2 size-4" />
- <span>{t("services")}</span>
- </CommandItem>
- <CommandItem
- onSelect={() => {
- navigate({ to: "/nodes" });
- setOpen(false);
- }}
- >
- <BoxIcon className="mr-2 h-4 w-4" />
- <span>{t("nodes")}</span>
- </CommandItem>
- <CommandItem
- onSelect={() => {
- navigate({ to: "/hosts" });
- setOpen(false);
- }}
- >
- <ServerCogIcon className="mr-2 h-4 w-4" />
- <span>{t("hosts")}</span>
- </CommandItem>
- </CommandGroup>
- <CommandSeparator />
- <CommandGroup heading="Settings">
- <CommandItem
- onSelect={() => {
- navigate({ to: "/settings" });
- setOpen(false);
- }}
- >
- <SettingsIcon className="mr-2 h-4 w-4" />
- <span>{t("settings")}</span>
- </CommandItem>
- </CommandGroup>
- </CommandList>
- </CommandDialog>
- </>
- );
+ const { isSudo } = useAuth();
+
+ return (
+ <>
+ <SearchBox onClick={() => setOpen(true)} />
+ <CommandDialog open={open} onOpenChange={setOpen}>
+ <CommandInput
+ placeholder="Type a command or search..."
+ className="focus:ring-0 ring-0 m-1 border-none"
+ />
+ <CommandList>
+ <CommandEmpty>No results found.</CommandEmpty>
+ <CommandGroup heading="Pages">
+ <CommandItem
+ onSelect={() => {
+ navigate({ to: "/users" });
+ setOpen(false);
+ }}
+ >
+ <UsersIcon className="mr-2 size-4" />
+ <span>{t("users")}</span>
+ </CommandItem>
+ {isSudo() && (
+ <>
+ <CommandItem
+ onSelect={() => {
+ navigate({ to: "/services" });
+ setOpen(false);
+ }}
+ >
+ <ServerIcon className="mr-2 size-4" />
+ <span>{t("services")}</span>
+ </CommandItem>
+ <CommandItem
+ onSelect={() => {
+ navigate({ to: "/nodes" });
+ setOpen(false);
+ }}
+ >
+ <BoxIcon className="mr-2 h-4 w-4" />
+ <span>{t("nodes")}</span>
+ </CommandItem>
+ <CommandItem
+ onSelect={() => {
+ navigate({ to: "/hosts" });
+ setOpen(false);
+ }}
+ >
+ <ServerCogIcon className="mr-2 h-4 w-4" />
+ <span>{t("hosts")}</span>
+ </CommandItem>
+ </>
+ )}
+ </CommandGroup>
+ {isSudo() && (
+ <>
+ <CommandSeparator />
+ <CommandGroup heading="Settings">
+ <CommandItem
+ onSelect={() => {
+ navigate({ to: "/settings" });
+ setOpen(false);
+ }}
+ >
+ <SettingsIcon className="mr-2 h-4 w-4" />
+ <span>{t("settings")}</span>
+ </CommandItem>
+ </CommandGroup>
+ </>
+ )}
+ </CommandList>
+ </CommandDialog>
+ </>
+ );
}
diff --git a/dashboard/src/features/sidebar/items.tsx b/dashboard/src/features/sidebar/items.tsx
index 1b3ee02..ed8cd83 100644
--- a/dashboard/src/features/sidebar/items.tsx
+++ b/dashboard/src/features/sidebar/items.tsx
@@ -45,3 +45,22 @@ export const sidebarItems: SidebarObject = {
},
]
};
+
+export const sidebarItemsNonSudoAdmin: SidebarObject = {
+ Dashboard: [
+ {
+ title: 'Home',
+ to: '/',
+ icon: <Home />,
+ isParent: false,
+ },
+ ],
+ Management: [
+ {
+ title: 'Users',
+ to: '/users',
+ icon: <UsersIcon />,
+ isParent: false,
+ },
+ ],
+};
diff --git a/dashboard/src/features/sidebar/sidebar.tsx b/dashboard/src/features/sidebar/sidebar.tsx
index 2650519..5cd6a17 100644
--- a/dashboard/src/features/sidebar/sidebar.tsx
+++ b/dashboard/src/features/sidebar/sidebar.tsx
@@ -5,8 +5,9 @@ import {
} from "@marzneshin/components";
import { useRouterState } from "@tanstack/react-router";
import type { FC } from "react";
-import { sidebarItems } from ".";
+import { sidebarItems as sidebarItemsSudoAdmin, sidebarItemsNonSudoAdmin } from ".";
import { cn } from "@marzneshin/utils";
+import { useAuth } from "@marzneshin/features/auth";
interface DashboardSidebarProps {
collapsed: boolean;
@@ -22,6 +23,7 @@ export const DashboardSidebar: FC<DashboardSidebarProps> = ({
open,
}) => {
const router = useRouterState();
+ const { isSudo } = useAuth();
const currentActivePath = router.location.pathname;
const isActive = (path: string) => {
if (path === "/") {
@@ -29,7 +31,7 @@ export const DashboardSidebar: FC<DashboardSidebarProps> = ({
}
return currentActivePath.startsWith(path);
};
-
+ const sidebarItems = isSudo() ? sidebarItemsSudoAdmin : sidebarItemsNonSudoAdmin
return (
<aside className="size-full py-4 px-4 ">
<nav className="size-full">
diff --git a/dashboard/src/features/sudo-routes/index.tsx b/dashboard/src/features/sudo-routes/index.tsx
new file mode 100644
index 0000000..1e95372
--- /dev/null
+++ b/dashboard/src/features/sudo-routes/index.tsx
@@ -0,0 +1,26 @@
+import { useAuth } from "@marzneshin/features/auth";
+import { useNavigate } from "@tanstack/react-router";
+import { useEffect , PropsWithChildren, FC} from "react";
+
+export const SudoRoute: FC<PropsWithChildren> = ({ children }) => {
+ const { isSudo, isLoggedIn } = useAuth();
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ const checkAccess = async () => {
+ const loggedIn = await isLoggedIn();
+ if (!loggedIn || !isSudo()) {
+ navigate({ to: '/login' });
+ }
+ };
+
+ checkAccess();
+ }, [isSudo, isLoggedIn, navigate]);
+
+ if (!isSudo) {
+ return null;
+ }
+
+ return children;
+};
+
diff --git a/dashboard/src/routes/_dashboard/hosts.lazy.tsx b/dashboard/src/routes/_dashboard/hosts.lazy.tsx
index b0902b0..fd49fb8 100644
--- a/dashboard/src/routes/_dashboard/hosts.lazy.tsx
+++ b/dashboard/src/routes/_dashboard/hosts.lazy.tsx
@@ -8,6 +8,7 @@ import {
Loading,
} from '@marzneshin/components'
import { InboundHostsTable } from '@marzneshin/features/hosts'
+import { SudoRoute } from '@marzneshin/features/sudo-routes'
import { createFileRoute, Outlet } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
import { Suspense } from 'react';
@@ -36,5 +37,5 @@ export const HostsPage = () => {
}
export const Route = createFileRoute('/_dashboard/hosts')({
- component: () => <HostsPage />
+ component: () => <SudoRoute><HostsPage /></SudoRoute>
})
diff --git a/dashboard/src/routes/_dashboard/nodes.lazy.tsx b/dashboard/src/routes/_dashboard/nodes.lazy.tsx
index 5e5195d..f0e8d88 100644
--- a/dashboard/src/routes/_dashboard/nodes.lazy.tsx
+++ b/dashboard/src/routes/_dashboard/nodes.lazy.tsx
@@ -15,6 +15,7 @@ import { Link, createFileRoute, Outlet } from '@tanstack/react-router'
import { Info } from 'lucide-react';
import { type FC, Suspense } from 'react';
import { useTranslation } from 'react-i18next';
+import { SudoRoute } from "@marzneshin/features/sudo-routes";
export const NodesPage: FC = () => {
const { t } = useTranslation();
@@ -48,5 +49,5 @@ export const NodesPage: FC = () => {
};
export const Route = createFileRoute('/_dashboard/nodes')({
- component: () => <NodesPage />
+ component: () => <SudoRoute> <NodesPage /> </SudoRoute>
})
diff --git a/dashboard/src/routes/_dashboard/services.lazy.tsx b/dashboard/src/routes/_dashboard/services.lazy.tsx
index 1c28541..7d6c788 100644
--- a/dashboard/src/routes/_dashboard/services.lazy.tsx
+++ b/dashboard/src/routes/_dashboard/services.lazy.tsx
@@ -10,6 +10,7 @@ import { ServicesTable } from "@marzneshin/features/services";
import { createFileRoute, Outlet } from "@tanstack/react-router";
import { type FC, Suspense } from "react";
import { useTranslation } from "react-i18next";
+import { SudoRoute } from "@marzneshin/features/sudo-routes";
export const ServicesPage: FC = () => {
const { t } = useTranslation();
@@ -31,5 +32,5 @@ export const ServicesPage: FC = () => {
};
export const Route = createFileRoute("/_dashboard/services")({
- component: () => <ServicesPage />,
+ component: () => <SudoRoute> <ServicesPage /> </SudoRoute>
});
diff --git a/dashboard/src/routes/_dashboard/settings.lazy.tsx b/dashboard/src/routes/_dashboard/settings.lazy.tsx
index 47acf3d..e600eae 100644
--- a/dashboard/src/routes/_dashboard/settings.lazy.tsx
+++ b/dashboard/src/routes/_dashboard/settings.lazy.tsx
@@ -4,6 +4,7 @@ import { CertificateWidget, ConfigurationWidget } from '@marzneshin/features/set
import { SubscriptionSettingsWidget } from '@marzneshin/features/subscription-settings';
import { createFileRoute } from '@tanstack/react-router';
import { useTranslation } from 'react-i18next'
+import { SudoRoute } from "@marzneshin/features/sudo-routes";
export const Settings = () => {
const { t } = useTranslation()
@@ -28,5 +29,5 @@ export const Settings = () => {
}
export const Route = createFileRoute('/_dashboard/settings')({
- component: () => <Settings />
+ component: () => <SudoRoute> <Settings /> </SudoRoute>
})