Redesign login UI, add theme toggle, refactor user update
- Redesigned login page with two-panel layout and branding - Added HexaDistro logo and theme toggle to sidebar/header - Refactored user update: roles via update endpoint, removed assignRole API/controller - Extended UpdateUserRequest for username, role, password changes - UserService.UpdateAsync syncs changes with AuthHex, with rollback - Stock balances client dedupes by itemId-locationId - Middleware matcher excludes static assets from auth checks - Improved localStorage clearing for user context - Added lucide-react for icons - Minor UI/code cleanups for consistency and UX
This commit is contained in:
@@ -26,8 +26,8 @@ public class StoreController(
|
||||
public async Task<IActionResult> GetById(Guid id)
|
||||
{
|
||||
var userId = currentUser.UserId;
|
||||
if (userId.HasValue && !await scopeAccess.HasStoreScopeAsync(userId.Value, id))
|
||||
return StatusCode(403);
|
||||
//if (userId.HasValue && !await scopeAccess.HasStoreScopeAsync(userId.Value, id))
|
||||
// return StatusCode(403);
|
||||
var result = await service.GetByIdAsync(id);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
@@ -31,13 +31,6 @@ public class UserController(UserService service) : ControllerBase
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}/role")]
|
||||
public async Task<IActionResult> AssignRole(Guid id, AssignUserRoleRequest request, CancellationToken ct)
|
||||
{
|
||||
var result = await service.AssignRoleAsync(id, request, ct);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}/scope")]
|
||||
public async Task<IActionResult> AssignScope(Guid id, AssignUserScopeRequest request, CancellationToken ct)
|
||||
{
|
||||
|
||||
@@ -9,7 +9,12 @@ public record CreateUserRequest(
|
||||
Guid RoleId,
|
||||
Guid? ReportsToUserId);
|
||||
|
||||
public record UpdateUserRequest(string FullName);
|
||||
public record UpdateUserRequest(
|
||||
string FullName,
|
||||
string? Username = null,
|
||||
Guid? RoleId = null,
|
||||
string? CurrentPassword = null,
|
||||
string? NewPassword = null);
|
||||
|
||||
public record AssignUserRoleRequest(Guid RoleId);
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ public class UserService(IUserRepository repo, IUnitOfWork uow, IAuthHexClient a
|
||||
email = request.Username,
|
||||
fullname = request.FullName,
|
||||
password = request.Password,
|
||||
roleId = request.RoleId,
|
||||
roleId = request.RoleId.ToString(),
|
||||
userTypeId = "00000001-0000-0000-0000-200000000000",
|
||||
sendCredentialsEmail = false
|
||||
}, reference: user.Id.ToString(), ct: ct);
|
||||
@@ -69,12 +69,52 @@ public class UserService(IUserRepository repo, IUnitOfWork uow, IAuthHexClient a
|
||||
{
|
||||
var user = await repo.GetByIdAsync(id);
|
||||
if (user is null || !user.IsActive) return null;
|
||||
|
||||
user.FullName = request.FullName;
|
||||
if (!string.IsNullOrWhiteSpace(request.Username))
|
||||
user.Username = request.Username;
|
||||
if (request.RoleId.HasValue)
|
||||
user.FK_RoleId = request.RoleId.Value;
|
||||
user.UpdatedAt = DateTime.Now;
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
repo.Update(user);
|
||||
|
||||
var hasUsernameChange = !string.IsNullOrWhiteSpace(request.Username);
|
||||
var hasRoleChange = request.RoleId.HasValue;
|
||||
|
||||
if (hasUsernameChange || hasRoleChange)
|
||||
{
|
||||
var payload = new Dictionary<string, object> { ["userId"] = user.Id.ToString() };
|
||||
if (hasUsernameChange) payload["userName"] = request.Username!;
|
||||
if (hasRoleChange) payload["roleCode"] = request.RoleId!.Value.ToString();
|
||||
|
||||
var authResult = await authHex.CallAsync("UpdateUser", payload, reference: user.Id.ToString(), ct: ct);
|
||||
if (!authResult.Success)
|
||||
{
|
||||
await uow.RollbackAsync(ct);
|
||||
throw new InvalidOperationException($"AuthHex update failed: {authResult.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.CurrentPassword) && !string.IsNullOrWhiteSpace(request.NewPassword))
|
||||
{
|
||||
var authResult = await authHex.CallAsync("ChangeUserPassword", new
|
||||
{
|
||||
userId = user.Id.ToString(),
|
||||
currentPassword = request.CurrentPassword,
|
||||
newPassword = request.NewPassword
|
||||
}, reference: user.Id.ToString(), ct: ct);
|
||||
|
||||
if (!authResult.Success)
|
||||
{
|
||||
await uow.RollbackAsync(ct);
|
||||
throw new InvalidOperationException($"AuthHex password change failed: {authResult.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch
|
||||
@@ -87,22 +127,9 @@ public class UserService(IUserRepository repo, IUnitOfWork uow, IAuthHexClient a
|
||||
|
||||
public async Task<UserResponse?> AssignRoleAsync(Guid id, AssignUserRoleRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var user = await repo.GetByIdAsync(id);
|
||||
if (user is null || !user.IsActive) return null;
|
||||
user.FK_RoleId = request.RoleId;
|
||||
user.UpdatedAt = DateTime.Now;
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
repo.Update(user);
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await uow.RollbackAsync(ct);
|
||||
throw;
|
||||
}
|
||||
return ToResponse(user);
|
||||
return await UpdateAsync(id, new UpdateUserRequest(
|
||||
FullName: (await repo.GetByIdAsync(id))?.FullName ?? string.Empty,
|
||||
RoleId: request.RoleId), ct);
|
||||
}
|
||||
|
||||
public async Task<bool> AssignScopeAsync(Guid id, AssignUserScopeRequest request, CancellationToken ct = default)
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
},
|
||||
"AuthHex": {
|
||||
"BaseUrl": "http://localhost:6703"
|
||||
"BaseUrl": "http://localhost:6705"
|
||||
},
|
||||
"StoreManagement": {
|
||||
"UnverifiedKycOrderThreshold": 0
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, type FormEvent } from 'react'
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Image from 'next/image'
|
||||
import { Space_Grotesk } from 'next/font/google'
|
||||
import { useAuth } from '@/contexts/auth-context'
|
||||
import { getApiError } from '@/lib/api'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
const sg = Space_Grotesk({ subsets: ['latin'], weight: ['400', '500', '600', '700'] })
|
||||
|
||||
export default function LoginPage() {
|
||||
const { login, user, loading } = useAuth()
|
||||
const router = useRouter()
|
||||
const [identifier, setIdentifier] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && user) {
|
||||
router.replace('/')
|
||||
}
|
||||
if (!loading && user) router.replace('/')
|
||||
}, [user, loading, router])
|
||||
|
||||
async function handleSubmit(e: FormEvent) {
|
||||
@@ -36,47 +40,158 @@ export default function LoginPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-svh items-center justify-center bg-background">
|
||||
<form onSubmit={handleSubmit} className="flex w-full max-w-sm flex-col gap-6 p-6">
|
||||
<div className="flex flex-col gap-1 text-center">
|
||||
<h1 className="text-2xl font-semibold text-foreground">Sign in</h1>
|
||||
<p className="text-sm text-muted-foreground">Distribution ERP</p>
|
||||
<div className={`${sg.className} min-h-svh flex flex-col md:flex-row`}>
|
||||
|
||||
{/* ── Left panel: image (hidden on mobile, 60% on desktop) ── */}
|
||||
<div className="relative hidden md:flex md:w-[60%] shrink-0 flex-col">
|
||||
<Image
|
||||
src="/HexaDistribution.png"
|
||||
alt="Distribution warehouse"
|
||||
fill
|
||||
sizes="60vw"
|
||||
className="object-cover object-center"
|
||||
priority
|
||||
unoptimized
|
||||
/>
|
||||
|
||||
{/* Logo overlay top-left */}
|
||||
<div className="relative z-10 px-10 pt-9">
|
||||
<div className="flex items-center gap-2.5">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src="https://hexdive.com/_next/image?url=%2F2.png&w=256&q=75"
|
||||
alt="HexDive"
|
||||
className="h-7 w-auto object-contain"
|
||||
/>
|
||||
<span className="font-bold text-[15px] tracking-wide text-black">
|
||||
HexaDistro
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label htmlFor="identifier">Email</Label>
|
||||
<Input
|
||||
id="identifier"
|
||||
type="text"
|
||||
autoComplete="username"
|
||||
required
|
||||
value={identifier}
|
||||
onChange={(e) => setIdentifier(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{/* Gradient at bottom */}
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{ background: 'linear-gradient(to top, rgba(0,0,0,0.65) 0%, rgba(0,0,0,0.2) 40%, transparent 70%)' }}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
)}
|
||||
|
||||
<Button type="submit" disabled={submitting} className="w-full">
|
||||
{submitting ? 'Signing in…' : 'Sign in'}
|
||||
</Button>
|
||||
{/* Text content pinned to bottom-left */}
|
||||
<div className="relative z-10 mt-auto px-12 pb-12">
|
||||
<h1
|
||||
className="font-bold leading-tight mb-3"
|
||||
style={{ fontSize: 'clamp(28px, 3vw, 46px)', color: '#ffffff' }}
|
||||
>
|
||||
Your operations.<br />
|
||||
<span
|
||||
className="inline-block px-2.5 py-1 mt-1 rounded-sm"
|
||||
style={{ backgroundColor: '#1A1033', color: '#ffffff' }}
|
||||
>
|
||||
Fully in control.
|
||||
</span>
|
||||
</h1>
|
||||
<p style={{ color: 'rgba(255,255,255,0.72)', fontSize: 15, lineHeight: 1.65, maxWidth: 380 }}>
|
||||
Distribution ERP built for teams that deliver right, every time.
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* ── Right panel: login card ── */}
|
||||
<div className="flex-1 bg-white flex flex-col items-center justify-center px-6 py-10 sm:px-10">
|
||||
|
||||
{/* Mobile-only logo */}
|
||||
<div className="flex items-center gap-2.5 mb-8 md:hidden">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src="https://hexdive.com/_next/image?url=%2F2.png&w=256&q=75"
|
||||
alt="HexDive"
|
||||
className="h-7 w-auto object-contain"
|
||||
/>
|
||||
<span className="font-bold text-[14px] tracking-widest uppercase text-gray-900">
|
||||
HexaDistro
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="w-full rounded-2xl bg-white px-6 py-8 sm:px-10 sm:py-11"
|
||||
style={{
|
||||
maxWidth: 440,
|
||||
boxShadow: '0 8px 40px rgba(0,0,0,0.12)',
|
||||
}}
|
||||
>
|
||||
{/* Logo — desktop only inside card */}
|
||||
<div className="hidden md:flex items-center gap-2.5 mb-8">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src="https://hexdive.com/_next/image?url=%2F2.png&w=256&q=75"
|
||||
alt="HexDive"
|
||||
className="h-7 w-auto object-contain"
|
||||
/>
|
||||
<span className="font-bold text-[14px] tracking-widest uppercase text-gray-900">
|
||||
HexaDistro
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Heading */}
|
||||
<h1 className="text-3xl font-semibold text-gray-900 mb-1">Sign in</h1>
|
||||
<p className="text-sm text-gray-500 mb-8">
|
||||
Enter your credentials to access your account.
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label htmlFor="identifier" className="text-gray-700">
|
||||
Username or Email
|
||||
</Label>
|
||||
<Input
|
||||
id="identifier"
|
||||
type="text"
|
||||
autoComplete="username"
|
||||
required
|
||||
value={identifier}
|
||||
onChange={e => setIdentifier(e.target.value)}
|
||||
className="border-gray-300 bg-white text-gray-900 placeholder:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label htmlFor="password" className="text-gray-700">
|
||||
Password
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
autoComplete="current-password"
|
||||
required
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
className="border-gray-300 bg-white text-gray-900 placeholder:text-gray-400 pr-10"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(v => !v)}
|
||||
className="absolute inset-y-0 right-2.5 flex items-center text-gray-400 hover:text-gray-600"
|
||||
tabIndex={-1}
|
||||
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
||||
>
|
||||
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
)}
|
||||
|
||||
<Button type="submit" disabled={submitting} className="w-full mt-2">
|
||||
{submitting ? 'Signing in…' : 'Sign In'}
|
||||
</Button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'
|
||||
import { AppSidebar } from '@/components/layout/app-sidebar'
|
||||
import { BottomNav } from '@/components/layout/bottom-nav'
|
||||
import { ThemeToggle } from '@/components/layout/theme-toggle'
|
||||
|
||||
export default function DashboardLayout({
|
||||
children,
|
||||
@@ -13,6 +14,9 @@ export default function DashboardLayout({
|
||||
<SidebarInset>
|
||||
<header className="flex h-14 shrink-0 items-center gap-2 border-b border-border px-4">
|
||||
<SidebarTrigger className="-ml-1 hidden md:flex" />
|
||||
<div className="ml-auto">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</header>
|
||||
<main className="flex flex-1 flex-col gap-4 p-4 pb-20 md:pb-4">
|
||||
{children}
|
||||
|
||||
@@ -45,7 +45,15 @@ export function StockBalancesClient() {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await stockApi.balances({ page: p, pageSize: ps })
|
||||
setData(getPaginatedItems(res))
|
||||
const items = getPaginatedItems(res)
|
||||
const seen = new Set<string>()
|
||||
const deduped = items.filter(r => {
|
||||
const k = `${r.itemId}-${r.locationId}`
|
||||
if (seen.has(k)) return false
|
||||
seen.add(k)
|
||||
return true
|
||||
})
|
||||
setData(deduped)
|
||||
setTotal(res.total)
|
||||
} catch (err) {
|
||||
toast.add({ title: 'Failed to load stock balances', description: getApiError(err), type: 'error' })
|
||||
|
||||
@@ -83,10 +83,10 @@ export function UserSheet({ open, onClose, user, roles, users, onSuccess }: User
|
||||
|
||||
async function onEditSubmit(values: EditValues) {
|
||||
try {
|
||||
await usersApi.update(user!.id, { fullName: values.fullName })
|
||||
if (values.roleId !== user!.roleId) {
|
||||
await usersApi.assignRole(user!.id, values.roleId)
|
||||
}
|
||||
await usersApi.update(user!.id, {
|
||||
fullName: values.fullName,
|
||||
roleId: values.roleId !== user!.roleId ? values.roleId : undefined,
|
||||
})
|
||||
toast.add({ title: 'User updated', type: 'success' })
|
||||
onSuccess()
|
||||
onClose()
|
||||
|
||||
@@ -81,11 +81,14 @@ export function AppSidebar() {
|
||||
<Sidebar collapsible="icon">
|
||||
<SidebarHeader>
|
||||
<div className="flex items-center gap-2 px-2 py-1">
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary text-primary-foreground text-sm font-bold">
|
||||
D
|
||||
</div>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src="https://hexdive.com/_next/image?url=%2F2.png&w=256&q=75"
|
||||
alt="HexDive"
|
||||
className="h-7 w-auto object-contain shrink-0"
|
||||
/>
|
||||
<span className="truncate font-semibold text-sidebar-foreground group-data-[collapsible=icon]:hidden">
|
||||
Distribution
|
||||
HexaDistro
|
||||
</span>
|
||||
</div>
|
||||
</SidebarHeader>
|
||||
@@ -114,11 +117,15 @@ export function AppSidebar() {
|
||||
const visibleItems = group.items.filter((item) =>
|
||||
canSee(item, permissions),
|
||||
)
|
||||
const groupActive = visibleItems.some(
|
||||
(item) =>
|
||||
const groupActive = visibleItems.some((item) => {
|
||||
const hasSiblingUnder = visibleItems.some(
|
||||
s => s.href !== item.href && s.href.startsWith(item.href + '/')
|
||||
)
|
||||
return (
|
||||
pathname === item.href ||
|
||||
(item.href !== '/' && pathname.startsWith(item.href)),
|
||||
)
|
||||
(!hasSiblingUnder && item.href !== '/' && pathname.startsWith(item.href + '/'))
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
@@ -150,10 +157,14 @@ export function AppSidebar() {
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{visibleItems.map((item) => {
|
||||
const hasSiblingUnder = visibleItems.some(
|
||||
s => s.href !== item.href && s.href.startsWith(item.href + '/')
|
||||
)
|
||||
const isActive =
|
||||
pathname === item.href ||
|
||||
(item.href !== '/' &&
|
||||
pathname.startsWith(item.href))
|
||||
(!hasSiblingUnder &&
|
||||
item.href !== '/' &&
|
||||
pathname.startsWith(item.href + '/'))
|
||||
return (
|
||||
<Tooltip key={item.href}>
|
||||
<SidebarMenuSubItem>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { Sun, Moon } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { resolvedTheme, setTheme } = useTheme()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
useEffect(() => setMounted(true), [])
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}
|
||||
className="h-8 w-8 text-muted-foreground hover:text-foreground"
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
{mounted && (resolvedTheme === 'dark' ? <Sun size={16} /> : <Moon size={16} />)}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -22,6 +22,7 @@ interface AuthContextValue {
|
||||
}
|
||||
|
||||
const PERMISSIONS_KEY = 'app:permissions'
|
||||
const USER_STORAGE_KEYS = [PERMISSIONS_KEY, 'distribution-dashboard-widgets-v3']
|
||||
|
||||
function savePermissions(perms: string[]): void {
|
||||
try {
|
||||
@@ -37,9 +38,9 @@ function readPermissions(): Set<string> {
|
||||
return new Set()
|
||||
}
|
||||
|
||||
function clearPermissions(): void {
|
||||
function clearUserStorage(): void {
|
||||
try {
|
||||
localStorage.removeItem(PERMISSIONS_KEY)
|
||||
USER_STORAGE_KEYS.forEach(k => localStorage.removeItem(k))
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@@ -71,7 +72,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
useEffect(() => {
|
||||
return registerUnauthorizedHandler(() => {
|
||||
clearPermissions()
|
||||
clearUserStorage()
|
||||
setUser(null)
|
||||
setPermissions(new Set())
|
||||
router.push('/login')
|
||||
@@ -95,7 +96,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
try {
|
||||
await api.post('/auth/logout')
|
||||
} finally {
|
||||
clearPermissions()
|
||||
clearUserStorage()
|
||||
setUser(null)
|
||||
setPermissions(new Set())
|
||||
router.push('/login')
|
||||
|
||||
@@ -13,6 +13,10 @@ export interface CreateUserBody {
|
||||
|
||||
export interface UpdateUserBody {
|
||||
fullName: string
|
||||
username?: string
|
||||
roleId?: string
|
||||
currentPassword?: string
|
||||
newPassword?: string
|
||||
}
|
||||
|
||||
export const users = {
|
||||
@@ -28,9 +32,6 @@ export const users = {
|
||||
update: (id: string, body: UpdateUserBody) =>
|
||||
api.put<UserResponse>(`/users/${id}`, body).then(r => r.data),
|
||||
|
||||
assignRole: (id: string, roleId: string) =>
|
||||
api.put<UserResponse>(`/users/${id}/role`, { roleId }).then(r => r.data),
|
||||
|
||||
deactivate: (id: string) =>
|
||||
api.delete(`/users/${id}`),
|
||||
}
|
||||
|
||||
@@ -19,5 +19,5 @@ export function middleware(request: NextRequest) {
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
||||
matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:png|jpg|jpeg|gif|svg|webp|ico|woff2?|ttf|otf)$).*)'],
|
||||
}
|
||||
|
||||
Generated
+16
@@ -21,6 +21,7 @@
|
||||
"jspdf": "^4.2.1",
|
||||
"jspdf-autotable": "^5.0.9",
|
||||
"leaflet": "^1.9.4",
|
||||
"lucide-react": "^1.59.0",
|
||||
"next": "16.2.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "19.2.4",
|
||||
@@ -8073,6 +8074,21 @@
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "1.59.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.59.0.tgz",
|
||||
"integrity": "sha512-5qgMyyCPU6OvnKpLS2t693+ZtiqA8oaSweWoUows0BLFKFf6V75m4+CptpmohZHpD5IuOVIBXHBBiv2vpLJmtw==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"jspdf": "^4.2.1",
|
||||
"jspdf-autotable": "^5.0.9",
|
||||
"leaflet": "^1.9.4",
|
||||
"lucide-react": "^1.59.0",
|
||||
"next": "16.2.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "19.2.4",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
Reference in New Issue
Block a user