feat: implemented rules to manager users
parent
64b990d248
commit
0444ca949e
|
@ -5,7 +5,8 @@ from .extensions import init_mongo, init_jwt, init_redis
|
|||
from .errors.handlers import register_error_handlers
|
||||
from .routes.usage_routes import usage_ns
|
||||
from .routes.auth_routes import auth_ns
|
||||
from .routes.billing_routes import billing_ns
|
||||
from .routes.billing_routes import billing_ns
|
||||
from .routes.users_routes import user_ns
|
||||
from flask_cors import CORS
|
||||
|
||||
def create_app():
|
||||
|
@ -37,7 +38,8 @@ def create_app():
|
|||
|
||||
api.add_namespace(usage_ns, path='/usage')
|
||||
api.add_namespace(auth_ns, path='/auth')
|
||||
api.add_namespace(billing_ns, path='/billing')
|
||||
api.add_namespace(billing_ns, path='/billing')
|
||||
api.add_namespace(user_ns, path='/users')
|
||||
|
||||
register_error_handlers(app)
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
from flask_bcrypt import generate_password_hash, check_password_hash
|
||||
from flask import current_app
|
||||
from pymongo import MongoClient
|
||||
from bson.objectid import ObjectId
|
||||
from bson.errors import InvalidId
|
||||
|
||||
class UserModel:
|
||||
def __init__(self):
|
||||
|
@ -13,7 +15,33 @@ class UserModel:
|
|||
return self.collection.insert_one(user)
|
||||
|
||||
def find_by_email(self, email):
|
||||
return self.collection.find_one({"email": email})
|
||||
return self.collection.find_one({"email": email})
|
||||
|
||||
def get_user_by_id(self, user_id):
|
||||
object_id = ObjectId(user_id)
|
||||
return self.collection.find_one({"_id": object_id},{"email": 1, "roles": 1,})
|
||||
|
||||
def update_user(self, user_id, update_data: dict):
|
||||
object_id = ObjectId(user_id)
|
||||
|
||||
if "password" in update_data:
|
||||
update_data["password"] = generate_password_hash(update_data["password"]).decode('utf-8')
|
||||
|
||||
result = self.collection.update_one(
|
||||
{"_id": object_id},
|
||||
{"$set": update_data}
|
||||
)
|
||||
|
||||
return result.modified_count > 0
|
||||
|
||||
def delete_user(self, user_id):
|
||||
object_id = ObjectId(user_id)
|
||||
result = self.collection.delete_one({"_id": object_id})
|
||||
return result.deleted_count > 0
|
||||
|
||||
def list_users(self):
|
||||
users = self.collection.find({}, {"email": 1, "roles": 1,})
|
||||
return users
|
||||
|
||||
def verify_password(self, hashed_password, plain_password):
|
||||
return check_password_hash(hashed_password, plain_password)
|
||||
|
|
|
@ -2,7 +2,13 @@ from flask_restx import fields, Namespace
|
|||
|
||||
auth_ns = Namespace('auth', description='Authentication')
|
||||
|
||||
signup_model = auth_ns.model('Signup', {
|
||||
signup_model = auth_ns.model('SignUp', {
|
||||
'email': fields.String(required=True),
|
||||
'password': fields.String(required=True)
|
||||
'password': fields.String(required=True),
|
||||
'roles': fields.List(fields.String, required=False, default=["user"])
|
||||
})
|
||||
|
||||
signin_model = auth_ns.model('SignIn', {
|
||||
'email': fields.String(required=True),
|
||||
'password': fields.String(required=True)
|
||||
})
|
|
@ -0,0 +1,8 @@
|
|||
from flask_restx import Resource, Namespace, fields
|
||||
|
||||
role_ns = Namespace('roles', description="Role management")
|
||||
|
||||
role_model = role_ns.model('Role', {
|
||||
'email': fields.String(required=True, description='User email'),
|
||||
'roles': fields.List(fields.String, required=True, description='List of roles')
|
||||
})
|
|
@ -0,0 +1,11 @@
|
|||
from flask_restx import fields, Namespace
|
||||
|
||||
user_ns = Namespace('user', description='Users')
|
||||
|
||||
update_user = user_ns.model('Update', {
|
||||
'email': fields.String(required=False, description='User email'),
|
||||
'password': fields.String(required=False, description='User password'),
|
||||
'roles': fields.String(required=False, description='User roles')
|
||||
})
|
||||
|
||||
|
|
@ -3,15 +3,21 @@ from flask import jsonify
|
|||
from werkzeug.exceptions import HTTPException
|
||||
from pydantic import ValidationError
|
||||
import traceback
|
||||
from bson.errors import InvalidId
|
||||
|
||||
|
||||
def register_error_handlers(app):
|
||||
@app.errorhandler(ValidationError)
|
||||
def handle_validation_error(e):
|
||||
return jsonify({"error": e.errors()}), 400
|
||||
|
||||
@app.errorhandler(InvalidId)
|
||||
def handle_invalid_id_error(e):
|
||||
return jsonify({"error": "Invalid ID format"}), 400
|
||||
|
||||
@app.errorhandler(HTTPException)
|
||||
def handle_http_exception(e):
|
||||
return jsonify({"errror": e.description}), e.code
|
||||
return jsonify({"error": e.description}), e.code
|
||||
|
||||
@app.errorhandler(Exception)
|
||||
def handle_unexpected_exception(e):
|
||||
|
|
|
@ -1,31 +1,50 @@
|
|||
from flask_restx import Namespace, Resource, fields
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from flask import request, current_app
|
||||
from flask_jwt_extended import create_access_token
|
||||
from app.db.models import UserModel
|
||||
from app.docs.auth_models import auth_ns, signup_model
|
||||
from app.docs.auth_models import auth_ns, signup_model, signin_model
|
||||
from app.schemas.auth_sigin_schema import SigInRequest
|
||||
from app.schemas.auth_sigup_schema import SigUpRequest
|
||||
|
||||
@auth_ns.route('/signup')
|
||||
class SignUp(Resource):
|
||||
@auth_ns.expect(signup_model)
|
||||
def post(self):
|
||||
data = request.get_json()
|
||||
user_model = UserModel()
|
||||
if user_model.find_by_email(data['email']):
|
||||
return {'message': 'User already exists'}, 400
|
||||
user_model.create_user(data['email'], data['password'])
|
||||
return {'message': 'success'}, 201
|
||||
|
||||
validated = SigUpRequest(**data)
|
||||
|
||||
roles = data.get("roles", [])
|
||||
|
||||
user_model = UserModel()
|
||||
if user_model.find_by_email(validated.email):
|
||||
return {'message': 'User already exists'}, 400
|
||||
|
||||
result = user_model.create_user(validated.email, validated.password)
|
||||
|
||||
user_model.update_user(result.inserted_id, {"email": validated.email, "roles": roles})
|
||||
|
||||
return {'message': 'success'}, 201
|
||||
|
||||
@auth_ns.route('/login')
|
||||
class Login(Resource):
|
||||
@auth_ns.expect(signup_model)
|
||||
@auth_ns.expect(signin_model)
|
||||
def post(self):
|
||||
data = request.get_json()
|
||||
|
||||
validated = SigInRequest(**data)
|
||||
|
||||
user_model = UserModel()
|
||||
user = user_model.find_by_email(data['email'])
|
||||
user = user_model.find_by_email(validated.email)
|
||||
|
||||
if not user or not user_model.verify_password(user['password'], data['password']):
|
||||
return {'message': 'Invalid credentials'}, 401
|
||||
return {'message': 'Invalid credentials'}, 401
|
||||
|
||||
roles = user.get("roles", []) if user else []
|
||||
|
||||
access_token = create_access_token(
|
||||
identity=user['email'],
|
||||
additional_claims={"roles": roles}
|
||||
)
|
||||
|
||||
access_token = create_access_token(identity=user['email'])
|
||||
return {'access_token': access_token}, 200
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
from flask_restx import Namespace, Resource, fields
|
||||
from flask import request, current_app
|
||||
import requests
|
||||
from bson import json_util
|
||||
from app.db.models import UserModel
|
||||
from flask_restx import Resource
|
||||
from flask import request
|
||||
import requests
|
||||
from app.docs.biling_models import billing_ns, product_model, update_price_model
|
||||
from app.config import Config
|
||||
|
||||
from app.utils.role_required import role_required
|
||||
from flask_jwt_extended import jwt_required
|
||||
|
||||
BILLING_API_URL = Config.BILLING_API_URL
|
||||
HEADERS = {
|
||||
|
@ -17,6 +16,8 @@ HEADERS = {
|
|||
@billing_ns.route('/product')
|
||||
class CreateProduct(Resource):
|
||||
@billing_ns.expect(product_model)
|
||||
@jwt_required()
|
||||
@role_required('admin', 'user')
|
||||
def post(self):
|
||||
data = request.get_json()
|
||||
|
||||
|
@ -27,6 +28,8 @@ class CreateProduct(Resource):
|
|||
@billing_ns.route('/product/<string:product_id>')
|
||||
class UpdateProduct(Resource):
|
||||
@billing_ns.expect(update_price_model)
|
||||
@jwt_required()
|
||||
@role_required('admin', 'user')
|
||||
def patch(self, product_id):
|
||||
data = request.get_json()
|
||||
|
||||
|
@ -36,6 +39,8 @@ class UpdateProduct(Resource):
|
|||
|
||||
@billing_ns.route('/products')
|
||||
class ListProducts(Resource):
|
||||
@jwt_required()
|
||||
@role_required('admin', 'user')
|
||||
def get(self):
|
||||
response = requests.get(url=f'{BILLING_API_URL}/billing/products', headers=HEADERS)
|
||||
return response.json(), response.status_code
|
|
@ -1,5 +1,4 @@
|
|||
import os
|
||||
import hashlib
|
||||
import os
|
||||
from bson import json_util
|
||||
from flask_restx import Resource
|
||||
from flask_jwt_extended import jwt_required
|
||||
|
@ -17,6 +16,7 @@ from app.docs.usage_models import (
|
|||
transcription_data_query_params, usage_ns)
|
||||
from app.utils.current_date import is_current_date
|
||||
from app.utils.hash_key import set_hash_key
|
||||
from app.utils.role_required import role_required
|
||||
|
||||
TMP_DIR = '/tmp'
|
||||
|
||||
|
@ -28,6 +28,7 @@ class TranscriptionExport(Resource):
|
|||
@usage_ns.response(400, 'Validation error')
|
||||
@usage_ns.response(404, 'File not found')
|
||||
@jwt_required()
|
||||
@role_required('admin', 'user')
|
||||
def get(self):
|
||||
"""
|
||||
Export transcription report in XLSX.
|
||||
|
@ -84,6 +85,7 @@ class TranscriptionUsageData(Resource):
|
|||
@usage_ns.response(200, 'success')
|
||||
@usage_ns.response(400, 'Validation error')
|
||||
@jwt_required()
|
||||
@role_required('admin', 'user')
|
||||
def get(self):
|
||||
"""
|
||||
Get transcription report data.
|
||||
|
@ -123,6 +125,7 @@ class TranscriptionModelPrices(Resource):
|
|||
@usage_ns.response(200, 'success')
|
||||
@usage_ns.response(400, 'Validation error')
|
||||
@jwt_required()
|
||||
@role_required('admin', 'user')
|
||||
def get(self):
|
||||
"""
|
||||
Get model pricing and supplier information
|
||||
|
@ -161,6 +164,7 @@ class UpdateUsageCost(Resource):
|
|||
@usage_ns.response(200, 'success')
|
||||
@usage_ns.response(400, 'Validation error')
|
||||
@jwt_required()
|
||||
@role_required('admin', 'user')
|
||||
def patch(self):
|
||||
"""
|
||||
Updates the total cost of using products by company
|
||||
|
@ -190,6 +194,7 @@ class UpdateModelPrice(Resource):
|
|||
@usage_ns.response(200, 'Sucess')
|
||||
@usage_ns.response(400, 'Validation error')
|
||||
@jwt_required()
|
||||
@role_required('admin', 'user')
|
||||
def patch(self, id):
|
||||
"""
|
||||
Updates the price of a specific model by ID.
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
from flask_restx import Resource
|
||||
from app.docs.user_models import user_ns, update_user
|
||||
from flask import current_app, request
|
||||
from bson import json_util
|
||||
from app.db.models import UserModel
|
||||
from app.schemas.update_user_schema import UpdateUserRequest
|
||||
from app.utils.role_required import role_required
|
||||
from flask_jwt_extended import jwt_required
|
||||
|
||||
@user_ns.route('')
|
||||
@user_ns.doc(security='Bearer Auth')
|
||||
@user_ns.response(200, 'success')
|
||||
class Users(Resource):
|
||||
|
||||
@jwt_required()
|
||||
@role_required('admin', 'user')
|
||||
def get(self):
|
||||
user_model = UserModel()
|
||||
users = user_model.list_users()
|
||||
return current_app.response_class(
|
||||
response=json_util.dumps({"success": True, "data": users}),
|
||||
mimetype='application/json'
|
||||
)
|
||||
|
||||
|
||||
@user_ns.route('/<string:user_id>')
|
||||
@user_ns.doc(security='Bearer Auth')
|
||||
@user_ns.response(200, 'success')
|
||||
@user_ns.response(400, 'Validation error')
|
||||
class User(Resource):
|
||||
def __init__(self, api=None, *args, **kwargs):
|
||||
super().__init__(api, *args, **kwargs)
|
||||
self.user_model = UserModel()
|
||||
|
||||
@jwt_required()
|
||||
@role_required('admin', 'user')
|
||||
def get(self, user_id):
|
||||
if user := self.user_model.get_user_by_id(user_id):
|
||||
|
||||
return current_app.response_class(
|
||||
response=json_util.dumps({"success": True, "user": user}),
|
||||
mimetype='application/json'
|
||||
)
|
||||
|
||||
return {"success": False, 'message': 'User not found'}, 404
|
||||
|
||||
|
||||
@user_ns.expect(update_user)
|
||||
@jwt_required()
|
||||
@role_required('admin', 'user')
|
||||
def patch(self, user_id):
|
||||
data = request.get_json()
|
||||
|
||||
validated = UpdateUserRequest(**data)
|
||||
|
||||
if not self.user_model.get_user_by_id(user_id):
|
||||
return {"success": False, 'message': 'User not found'}, 404
|
||||
|
||||
update_data = validated.model_dump(exclude_none=True)
|
||||
|
||||
self.user_model.update_user(user_id, update_data)
|
||||
|
||||
return {"success": True, 'message': f'User {user_id} updated!'}, 200
|
||||
|
||||
|
||||
@jwt_required()
|
||||
@role_required('admin', 'user')
|
||||
def delete(self, user_id):
|
||||
|
||||
if not self.user_model.get_user_by_id(user_id):
|
||||
return {"success": False, 'message': 'User not found'}, 404
|
||||
|
||||
self.user_model.delete_user(user_id)
|
||||
|
||||
return {"success": True, 'message': f'User {user_id} deleted!'}, 200
|
|
@ -0,0 +1,5 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
class SigInRequest(BaseModel):
|
||||
email: str
|
||||
password: str
|
|
@ -0,0 +1,7 @@
|
|||
from pydantic import BaseModel
|
||||
from typing import List, Literal
|
||||
|
||||
class SigUpRequest(BaseModel):
|
||||
email: str
|
||||
password: str
|
||||
roles: List[Literal["admin", "user"]]
|
|
@ -0,0 +1,7 @@
|
|||
from pydantic import BaseModel
|
||||
from typing import List, Optional, Literal
|
||||
|
||||
class UpdateUserRequest(BaseModel):
|
||||
email: Optional[str] = None
|
||||
password: Optional[str] = None
|
||||
roles: Optional[List[str]] = None
|
|
@ -0,0 +1,16 @@
|
|||
from functools import wraps
|
||||
from flask_jwt_extended import get_jwt
|
||||
from flask import abort
|
||||
|
||||
|
||||
def role_required(*required_roles):
|
||||
def wrapper(fn):
|
||||
@wraps(fn)
|
||||
def decorator(*args, **kwargs):
|
||||
claims = get_jwt()
|
||||
user_roles = claims.get("roles", [])
|
||||
if not any(role in user_roles for role in required_roles):
|
||||
abort(403, description="Access forbidden: insufficient role")
|
||||
return fn(*args, **kwargs)
|
||||
return decorator
|
||||
return wrapper
|
|
@ -8,22 +8,38 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
|||
import { LogOut } from "lucide-react"
|
||||
import TranscriptionTable from "@/components/transcription-table"
|
||||
import ModelPricesTable from "@/components/model-prices-table"
|
||||
import CostUpdateForm from "@/components/cost-update-form"
|
||||
import ProductManagement from "@/components/product-management"
|
||||
import UserManagement from "@/components/user-management"
|
||||
import { isAdmin, isTokenExpired, getCurrentUserEmail, getUserRoles } from "@/lib/auth"
|
||||
|
||||
export default function Dashboard() {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [userIsAdmin, setUserIsAdmin] = useState(false)
|
||||
const [userEmail, setUserEmail] = useState<string | null>(null)
|
||||
const [userRoles, setUserRoles] = useState<string[]>([])
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem("access_token")
|
||||
if (!token) {
|
||||
|
||||
if (!token || isTokenExpired()) {
|
||||
// Token não existe ou expirou
|
||||
localStorage.removeItem("access_token")
|
||||
router.push("/")
|
||||
} else {
|
||||
setIsAuthenticated(true)
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Token válido, obter informações do usuário
|
||||
const email = getCurrentUserEmail()
|
||||
const roles = getUserRoles()
|
||||
const adminStatus = isAdmin()
|
||||
|
||||
setUserEmail(email)
|
||||
setUserRoles(roles)
|
||||
setUserIsAdmin(adminStatus)
|
||||
setIsAuthenticated(true)
|
||||
setIsLoading(false)
|
||||
}, [router])
|
||||
|
||||
const handleLogout = () => {
|
||||
|
@ -48,7 +64,15 @@ export default function Dashboard() {
|
|||
<header className="bg-white shadow">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center py-6">
|
||||
<h1 className="text-3xl font-bold text-gray-900">Dashboard - Custo de Produtos</h1>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Dashboard - Sistema de Transcrição</h1>
|
||||
{userEmail && (
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
Logado como: <span className="font-medium">{userEmail}</span>
|
||||
{userRoles.length > 0 && <span className="ml-2">({userRoles.join(", ")})</span>}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Button onClick={handleLogout} variant="outline">
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
Sair
|
||||
|
@ -59,11 +83,11 @@ export default function Dashboard() {
|
|||
|
||||
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||
<Tabs defaultValue="transcription" className="space-y-6">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsList className={`grid w-full ${userIsAdmin ? "grid-cols-4" : "grid-cols-3"}`}>
|
||||
<TabsTrigger value="transcription">Dados de Transcrição</TabsTrigger>
|
||||
<TabsTrigger value="models">Preços dos Modelos</TabsTrigger>
|
||||
{/* <TabsTrigger value="costs">Atualizar Custos</TabsTrigger> */}
|
||||
<TabsTrigger value="products">Produtos</TabsTrigger>
|
||||
{userIsAdmin && <TabsTrigger value="users">Usuários</TabsTrigger>}
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="transcription">
|
||||
|
@ -90,18 +114,6 @@ export default function Dashboard() {
|
|||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="costs">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Atualizar Custos</CardTitle>
|
||||
<CardDescription>Atualize os custos de uso dos produtos</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CostUpdateForm />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="products">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
@ -113,6 +125,20 @@ export default function Dashboard() {
|
|||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{userIsAdmin && (
|
||||
<TabsContent value="users">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Gerenciar Usuários</CardTitle>
|
||||
<CardDescription>Gerencie usuários e suas permissões no sistema</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<UserManagement />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { Check } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
))
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||
|
||||
export { Checkbox }
|
|
@ -1,66 +0,0 @@
|
|||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-card text-card-foreground",
|
||||
destructive:
|
||||
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Alert({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert"
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-title"
|
||||
className={cn(
|
||||
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-description"
|
||||
className={cn(
|
||||
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
|
@ -1,46 +0,0 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
|
@ -1,59 +0,0 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
|
@ -1,92 +0,0 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
|
@ -1,24 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Label }
|
|
@ -1,185 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default"
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "popper",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
|
@ -1,116 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="table-container"
|
||||
className="relative w-full overflow-x-auto"
|
||||
>
|
||||
<table
|
||||
data-slot="table"
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||
return (
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
className={cn("[&_tr]:border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||
return (
|
||||
<tbody
|
||||
data-slot="table-body"
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||
return (
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
className={cn(
|
||||
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
return (
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn(
|
||||
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
return (
|
||||
<th
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn(
|
||||
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCaption({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"caption">) {
|
||||
return (
|
||||
<caption
|
||||
data-slot="table-caption"
|
||||
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
className={cn("flex flex-col gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
className={cn(
|
||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
data-slot="tabs-content"
|
||||
className={cn("flex-1 outline-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
|
@ -1,18 +0,0 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Textarea }
|
|
@ -0,0 +1,523 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Edit2, Plus, Save, X, Loader2, RefreshCw, Users, Shield, Trash2 } from "lucide-react"
|
||||
import { isAdmin, getCurrentUserEmail } from "@/lib/auth"
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000/api/v1"
|
||||
|
||||
interface User {
|
||||
_id: {
|
||||
$oid: string
|
||||
}
|
||||
email: string
|
||||
roles: string[]
|
||||
}
|
||||
|
||||
interface UsersResponse {
|
||||
success: boolean
|
||||
data: User[]
|
||||
}
|
||||
|
||||
interface UserResponse {
|
||||
success: boolean
|
||||
user: User
|
||||
}
|
||||
|
||||
interface CreateUserRequest {
|
||||
email: string
|
||||
password: string
|
||||
roles: string[]
|
||||
}
|
||||
|
||||
interface UpdateUserRequest {
|
||||
email: string
|
||||
password?: string
|
||||
roles: string[]
|
||||
}
|
||||
|
||||
interface DeleteUserResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
}
|
||||
|
||||
interface UpdateUserResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
}
|
||||
|
||||
// Roles disponíveis no sistema
|
||||
const AVAILABLE_ROLES = [
|
||||
{ id: "admin", label: "Administrador", description: "Acesso total ao sistema" },
|
||||
{ id: "user", label: "Usuário", description: "Acesso parcial, não pode gerenciar usuários" },
|
||||
]
|
||||
|
||||
export default function UserManagement() {
|
||||
const [users, setUsers] = useState<User[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState("")
|
||||
const [success, setSuccess] = useState("")
|
||||
|
||||
// Estados para criação de usuário
|
||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
const [createForm, setCreateForm] = useState<CreateUserRequest>({
|
||||
email: "",
|
||||
password: "",
|
||||
roles: [],
|
||||
})
|
||||
|
||||
// Estados para edição de usuário
|
||||
const [editingUser, setEditingUser] = useState<string | null>(null)
|
||||
const [editForm, setEditForm] = useState<UpdateUserRequest>({
|
||||
email: "",
|
||||
password: "",
|
||||
roles: [],
|
||||
})
|
||||
const [isUpdating, setIsUpdating] = useState(false)
|
||||
|
||||
// Estados para exclusão
|
||||
const [isDeleting, setIsDeleting] = useState<string | null>(null)
|
||||
|
||||
const getAuthHeaders = () => {
|
||||
const token = localStorage.getItem("access_token")
|
||||
return {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
}
|
||||
|
||||
const fetchUsers = async () => {
|
||||
setIsLoading(true)
|
||||
setError("")
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/users`, {
|
||||
method: "GET",
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const result: UsersResponse = await response.json()
|
||||
setUsers(result.data || [])
|
||||
} else {
|
||||
const errorData = await response.json()
|
||||
setError(errorData.message || "Erro ao buscar usuários")
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Erro de conexão com o servidor")
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const createUser = async () => {
|
||||
if (!createForm.email || !createForm.password || createForm.roles.length === 0) {
|
||||
setError("Preencha todos os campos obrigatórios")
|
||||
return
|
||||
}
|
||||
|
||||
setIsCreating(true)
|
||||
setError("")
|
||||
setSuccess("")
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/signup`, {
|
||||
method: "POST",
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(createForm),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json()
|
||||
setSuccess("Usuário criado com sucesso!")
|
||||
setCreateForm({ email: "", password: "", roles: [] })
|
||||
setIsCreateDialogOpen(false)
|
||||
await fetchUsers()
|
||||
} else {
|
||||
const errorData = await response.json()
|
||||
setError(errorData.message || "Erro ao criar usuário")
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Erro de conexão com o servidor")
|
||||
} finally {
|
||||
setIsCreating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const updateUser = async (userId: string) => {
|
||||
if (!editForm.email || editForm.roles.length === 0) {
|
||||
setError("Preencha todos os campos obrigatórios")
|
||||
return
|
||||
}
|
||||
|
||||
setIsUpdating(true)
|
||||
setError("")
|
||||
setSuccess("")
|
||||
|
||||
try {
|
||||
const updateData: UpdateUserRequest = {
|
||||
email: editForm.email,
|
||||
roles: editForm.roles,
|
||||
}
|
||||
|
||||
// Só incluir password se foi preenchido
|
||||
if (editForm.password && editForm.password.trim() !== "") {
|
||||
updateData.password = editForm.password
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/users/${userId}`, {
|
||||
method: "PATCH",
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(updateData),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const result: UpdateUserResponse = await response.json()
|
||||
setSuccess("Usuário atualizado com sucesso!")
|
||||
setEditingUser(null)
|
||||
setEditForm({ email: "", password: "", roles: [] })
|
||||
await fetchUsers()
|
||||
} else {
|
||||
const errorData = await response.json()
|
||||
setError(errorData.message || "Erro ao atualizar usuário")
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Erro de conexão com o servidor")
|
||||
} finally {
|
||||
setIsUpdating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteUser = async (userId: string) => {
|
||||
if (!confirm("Tem certeza que deseja excluir este usuário? Esta ação não pode ser desfeita.")) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsDeleting(userId)
|
||||
setError("")
|
||||
setSuccess("")
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/users/${userId}`, {
|
||||
method: "DELETE",
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const result: DeleteUserResponse = await response.json()
|
||||
setSuccess("Usuário excluído com sucesso!")
|
||||
await fetchUsers()
|
||||
} else {
|
||||
const errorData = await response.json()
|
||||
setError(errorData.message || "Erro ao excluir usuário")
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Erro de conexão com o servidor")
|
||||
} finally {
|
||||
setIsDeleting(null)
|
||||
}
|
||||
}
|
||||
|
||||
const startEditUser = (user: User) => {
|
||||
setEditingUser(user._id.$oid)
|
||||
setEditForm({
|
||||
email: user.email,
|
||||
password: "", // Deixar vazio para não alterar a senha
|
||||
roles: [...user.roles],
|
||||
})
|
||||
}
|
||||
|
||||
const cancelEditUser = () => {
|
||||
setEditingUser(null)
|
||||
setEditForm({ email: "", password: "", roles: [] })
|
||||
}
|
||||
|
||||
const handleEditRoleChange = (roleId: string, checked: boolean | "indeterminate") => {
|
||||
if (checked === true) {
|
||||
setEditForm((prev) => ({ ...prev, roles: [...prev.roles, roleId] }))
|
||||
} else {
|
||||
setEditForm((prev) => ({ ...prev, roles: prev.roles.filter((role) => role !== roleId) }))
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreateRoleChange = (roleId: string, checked: boolean | "indeterminate") => {
|
||||
if (checked === true) {
|
||||
setCreateForm((prev) => ({ ...prev, roles: [...prev.roles, roleId] }))
|
||||
} else {
|
||||
setCreateForm((prev) => ({ ...prev, roles: prev.roles.filter((role) => role !== roleId) }))
|
||||
}
|
||||
}
|
||||
|
||||
const getRoleBadgeVariant = (role: string) => {
|
||||
switch (role) {
|
||||
case "admin":
|
||||
return "destructive" as const
|
||||
case "financeiro":
|
||||
return "default" as const
|
||||
case "user":
|
||||
return "secondary" as const
|
||||
default:
|
||||
return "outline" as const
|
||||
}
|
||||
}
|
||||
|
||||
const getRoleLabel = (roleId: string) => {
|
||||
const role = AVAILABLE_ROLES.find((r) => r.id === roleId)
|
||||
return role ? role.label : roleId
|
||||
}
|
||||
|
||||
const canDeleteUser = (user: User) => {
|
||||
const currentUserEmail = getCurrentUserEmail()
|
||||
// Não permitir que o usuário delete a si mesmo
|
||||
return user.email !== currentUserEmail
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers()
|
||||
}, [])
|
||||
|
||||
// Verificar se o usuário tem permissão para gerenciar usuários
|
||||
if (!isAdmin()) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<div className="rounded-full bg-red-100 p-6 mb-4">
|
||||
<Shield className="h-8 w-8 text-red-600" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium mb-2">Acesso Negado</h3>
|
||||
<p className="text-gray-500 max-w-md">
|
||||
Você não tem permissão para acessar o gerenciamento de usuários. Entre em contato com um administrador.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Cabeçalho com botão de criar */}
|
||||
<div className="flex justify-end items-center">
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={fetchUsers} disabled={isLoading} variant="outline">
|
||||
{isLoading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <RefreshCw className="mr-2 h-4 w-4" />}
|
||||
Atualizar
|
||||
</Button>
|
||||
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Novo Usuário
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Criar Novo Usuário</DialogTitle>
|
||||
<DialogDescription>Preencha as informações do novo usuário e suas permissões</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email *</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={createForm.email}
|
||||
onChange={(e) => setCreateForm((prev) => ({ ...prev, email: e.target.value }))}
|
||||
placeholder="usuario@exemplo.com"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Senha *</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={createForm.password}
|
||||
onChange={(e) => setCreateForm((prev) => ({ ...prev, password: e.target.value }))}
|
||||
placeholder="Senha do usuário"
|
||||
minLength={8}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Permissões *</Label>
|
||||
<div className="space-y-2">
|
||||
{AVAILABLE_ROLES.map((role) => (
|
||||
<div key={role.id} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={`create-${role.id}`}
|
||||
checked={createForm.roles.includes(role.id)}
|
||||
onCheckedChange={(checked) => handleCreateRoleChange(role.id, checked as boolean)}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<Label htmlFor={`create-${role.id}`} className="text-sm font-medium">
|
||||
{role.label}
|
||||
</Label>
|
||||
<p className="text-xs text-gray-500">{role.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 pt-4">
|
||||
<Button onClick={createUser} disabled={isCreating} className="flex-1">
|
||||
{isCreating ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Save className="mr-2 h-4 w-4" />}
|
||||
Criar Usuário
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setIsCreateDialogOpen(false)} disabled={isCreating}>
|
||||
Cancelar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<Alert>
|
||||
<AlertDescription>{success}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Tabela de Usuários */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Users className="h-5 w-5" />
|
||||
Lista de Usuários ({users.length} usuários)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{users.length > 0 ? (
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>Permissões</TableHead>
|
||||
<TableHead>Ações</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{users.map((user) => (
|
||||
<TableRow key={user._id.$oid}>
|
||||
<TableCell>
|
||||
{editingUser === user._id.$oid ? (
|
||||
<Input
|
||||
type="email"
|
||||
value={editForm.email}
|
||||
onChange={(e) => setEditForm((prev) => ({ ...prev, email: e.target.value }))}
|
||||
placeholder="Email do usuário"
|
||||
/>
|
||||
) : (
|
||||
<div className="font-medium">{user.email}</div>
|
||||
)}
|
||||
{editingUser === user._id.$oid && (
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
type="password"
|
||||
value={editForm.password}
|
||||
onChange={(e) => setEditForm((prev) => ({ ...prev, password: e.target.value }))}
|
||||
placeholder="Nova senha (deixe vazio para não alterar)"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{editingUser === user._id.$oid ? (
|
||||
<div className="space-y-2">
|
||||
{AVAILABLE_ROLES.map((role) => (
|
||||
<div key={role.id} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={`edit-${role.id}-${user._id.$oid}`}
|
||||
checked={editForm.roles.includes(role.id)}
|
||||
onCheckedChange={(checked) => handleEditRoleChange(role.id, checked as boolean)}
|
||||
/>
|
||||
<Label htmlFor={`edit-${role.id}-${user._id.$oid}`} className="text-sm">
|
||||
{role.label}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{user.roles.map((role) => (
|
||||
<Badge key={role} variant={getRoleBadgeVariant(role)}>
|
||||
{getRoleLabel(role)}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
{editingUser === user._id.$oid ? (
|
||||
<>
|
||||
<Button size="sm" onClick={() => updateUser(user._id.$oid)} disabled={isUpdating}>
|
||||
{isUpdating ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Save className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={cancelEditUser} disabled={isUpdating}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button size="sm" variant="outline" onClick={() => startEditUser(user)}>
|
||||
<Edit2 className="h-4 w-4" />
|
||||
</Button>
|
||||
{canDeleteUser(user) && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => deleteUser(user._id.$oid)}
|
||||
disabled={isDeleting === user._id.$oid}
|
||||
>
|
||||
{isDeleting === user._id.$oid ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
{isLoading ? "Carregando..." : "Nenhum usuário encontrado."}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
interface JWTPayload {
|
||||
fresh: boolean
|
||||
iat: number
|
||||
jti: string
|
||||
type: string
|
||||
sub: string
|
||||
nbf: number
|
||||
csrf: string
|
||||
exp: number
|
||||
roles: string[]
|
||||
}
|
||||
|
||||
export function decodeJWT(token: string): JWTPayload | null {
|
||||
try {
|
||||
// JWT tem 3 partes separadas por pontos: header.payload.signature
|
||||
const parts = token.split(".")
|
||||
if (parts.length !== 3) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Decodificar a parte do payload (segunda parte)
|
||||
const payload = parts[1]
|
||||
|
||||
// Adicionar padding se necessário para base64
|
||||
const paddedPayload = payload + "=".repeat((4 - (payload.length % 4)) % 4)
|
||||
|
||||
// Decodificar de base64
|
||||
const decodedPayload = atob(paddedPayload)
|
||||
|
||||
// Parse do JSON
|
||||
const parsedPayload: JWTPayload = JSON.parse(decodedPayload)
|
||||
|
||||
return parsedPayload
|
||||
} catch (error) {
|
||||
console.error("Erro ao decodificar JWT:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function getUserRoles(): string[] {
|
||||
try {
|
||||
const token = localStorage.getItem("access_token")
|
||||
if (!token) {
|
||||
return []
|
||||
}
|
||||
|
||||
const payload = decodeJWT(token)
|
||||
return payload?.roles || []
|
||||
} catch (error) {
|
||||
console.error("Erro ao obter roles do usuário:", error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export function hasRole(role: string): boolean {
|
||||
const roles = getUserRoles()
|
||||
return roles.includes(role)
|
||||
}
|
||||
|
||||
export function isAdmin(): boolean {
|
||||
return hasRole("admin")
|
||||
}
|
||||
|
||||
export function getCurrentUserEmail(): string | null {
|
||||
try {
|
||||
const token = localStorage.getItem("access_token")
|
||||
if (!token) {
|
||||
return null
|
||||
}
|
||||
|
||||
const payload = decodeJWT(token)
|
||||
return payload?.sub || null
|
||||
} catch (error) {
|
||||
console.error("Erro ao obter email do usuário:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function isTokenExpired(): boolean {
|
||||
try {
|
||||
const token = localStorage.getItem("access_token")
|
||||
if (!token) {
|
||||
return true
|
||||
}
|
||||
|
||||
const payload = decodeJWT(token)
|
||||
if (!payload) {
|
||||
return true
|
||||
}
|
||||
|
||||
// exp está em segundos, Date.now() está em milissegundos
|
||||
const currentTime = Math.floor(Date.now() / 1000)
|
||||
return payload.exp < currentTime
|
||||
} catch (error) {
|
||||
console.error("Erro ao verificar expiração do token:", error)
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@
|
|||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
|
@ -952,6 +953,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-checkbox": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.2.tgz",
|
||||
"integrity": "sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.2",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-presence": "1.1.4",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"@radix-ui/react-use-previous": "1.1.1",
|
||||
"@radix-ui/react-use-size": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collection": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
|
|
Loading…
Reference in New Issue