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, getAuthToken: () => string | null; + isSudo: () => boolean; setAuthToken: (token: string) => void; removeAuthToken: () => void; + setSudo: (isSudo: boolean) => void; } export const useAuth = create( subscribeWithSelector(() => ({ 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(''); 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 ( - <> - setOpen(true)} /> - - - - No results found. - - { - navigate({ to: "/users" }); - setOpen(false); - }} - > - - {t("users")} - - { - navigate({ to: "/services" }); - setOpen(false); - }} - > - - {t("services")} - - { - navigate({ to: "/nodes" }); - setOpen(false); - }} - > - - {t("nodes")} - - { - navigate({ to: "/hosts" }); - setOpen(false); - }} - > - - {t("hosts")} - - - - - { - navigate({ to: "/settings" }); - setOpen(false); - }} - > - - {t("settings")} - - - - - - ); + const { isSudo } = useAuth(); + + return ( + <> + setOpen(true)} /> + + + + No results found. + + { + navigate({ to: "/users" }); + setOpen(false); + }} + > + + {t("users")} + + {isSudo() && ( + <> + { + navigate({ to: "/services" }); + setOpen(false); + }} + > + + {t("services")} + + { + navigate({ to: "/nodes" }); + setOpen(false); + }} + > + + {t("nodes")} + + { + navigate({ to: "/hosts" }); + setOpen(false); + }} + > + + {t("hosts")} + + + )} + + {isSudo() && ( + <> + + + { + navigate({ to: "/settings" }); + setOpen(false); + }} + > + + {t("settings")} + + + + )} + + + + ); } 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: , + isParent: false, + }, + ], + Management: [ + { + title: 'Users', + to: '/users', + icon: , + 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 = ({ 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 = ({ } return currentActivePath.startsWith(path); }; - + const sidebarItems = isSudo() ? sidebarItemsSudoAdmin : sidebarItemsNonSudoAdmin return (