Add Hermes API and MCP access
This commit is contained in:
50
mini-services/hermes-mcp/README.md
Normal file
50
mini-services/hermes-mcp/README.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# OptiqueStock Hermes MCP
|
||||
|
||||
This MCP server gives a Hermes agent safe, token-scoped access to OptiqueStock.
|
||||
|
||||
## Environment
|
||||
|
||||
Set the same key on the Next.js app and the MCP server:
|
||||
|
||||
```bash
|
||||
HERMES_API_KEY=change-me
|
||||
OPTICZ_API_BASE=http://192.168.1.30:3000
|
||||
```
|
||||
|
||||
In local development only, the fallback key is:
|
||||
|
||||
```text
|
||||
hermes-demo-key-change-me
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
node mini-services/hermes-mcp/server.mjs
|
||||
```
|
||||
|
||||
## MCP Tools
|
||||
|
||||
- `opticz_status`
|
||||
- `opticz_summary`
|
||||
- `opticz_search_clients`
|
||||
- `opticz_search_products`
|
||||
- `opticz_create_client`
|
||||
- `opticz_create_repair_sale`
|
||||
|
||||
## Direct REST API
|
||||
|
||||
All endpoints require:
|
||||
|
||||
```http
|
||||
Authorization: Bearer <HERMES_API_KEY>
|
||||
```
|
||||
|
||||
Available endpoints:
|
||||
|
||||
- `GET /api/hermes/status`
|
||||
- `GET /api/hermes/summary`
|
||||
- `GET /api/hermes/clients?q=amina`
|
||||
- `POST /api/hermes/clients`
|
||||
- `GET /api/hermes/products?q=monture`
|
||||
- `POST /api/hermes/sales/repair`
|
||||
242
mini-services/hermes-mcp/server.mjs
Normal file
242
mini-services/hermes-mcp/server.mjs
Normal file
@@ -0,0 +1,242 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const apiBase = (process.env.OPTICZ_API_BASE || 'http://127.0.0.1:3000').replace(/\/$/, '')
|
||||
const apiKey = process.env.HERMES_API_KEY || 'hermes-demo-key-change-me'
|
||||
|
||||
const tools = [
|
||||
{
|
||||
name: 'opticz_status',
|
||||
description: 'Check OptiqueStock Hermes API health.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'opticz_summary',
|
||||
description: 'Get store counts, low stock products, pending workshop count, and recent sales.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'opticz_search_clients',
|
||||
description: 'Search OptiqueStock clients by name, phone, or email.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
q: { type: 'string', description: 'Search query.' },
|
||||
limit: { type: 'number', description: 'Maximum results, default 20, max 50.' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'opticz_search_products',
|
||||
description: 'Search active products by reference, designation, brand, or category.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
q: { type: 'string', description: 'Search query.' },
|
||||
category: { type: 'string', description: 'Optional category filter, e.g. MONTURE or VERRE.' },
|
||||
inStockOnly: { type: 'boolean', description: 'Only include products with stock. Defaults true.' },
|
||||
limit: { type: 'number', description: 'Maximum results, default 25, max 75.' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'opticz_create_client',
|
||||
description: 'Create a client, or return the existing client when the phone number already exists.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
required: ['nom', 'prenom', 'telephone'],
|
||||
properties: {
|
||||
nom: { type: 'string' },
|
||||
prenom: { type: 'string' },
|
||||
telephone: { type: 'string' },
|
||||
email: { type: 'string' },
|
||||
adresse: { type: 'string' },
|
||||
ville: { type: 'string' },
|
||||
codePostal: { type: 'string' },
|
||||
notes: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'opticz_create_repair_sale',
|
||||
description: 'Create a paid repair sale for an existing client.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
required: ['clientId', 'repairType', 'repairDescription', 'repairPrice'],
|
||||
properties: {
|
||||
clientId: { type: 'string' },
|
||||
repairType: { type: 'string' },
|
||||
repairDescription: { type: 'string' },
|
||||
repairPrice: { type: 'number' },
|
||||
additionalCharges: { type: 'number' },
|
||||
paymentMode: {
|
||||
type: 'string',
|
||||
enum: ['ESPECES', 'CARTE', 'CHEQUE', 'VIREMENT', 'BON_CAISSE'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
async function api(path, options = {}) {
|
||||
const response = await fetch(`${apiBase}${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers || {}),
|
||||
},
|
||||
})
|
||||
const text = await response.text()
|
||||
const data = text ? JSON.parse(text) : null
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data?.error || `HTTP ${response.status}`)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
function query(params) {
|
||||
const search = new URLSearchParams()
|
||||
for (const [key, value] of Object.entries(params || {})) {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
search.set(key, String(value))
|
||||
}
|
||||
}
|
||||
const value = search.toString()
|
||||
return value ? `?${value}` : ''
|
||||
}
|
||||
|
||||
async function callTool(name, args) {
|
||||
switch (name) {
|
||||
case 'opticz_status':
|
||||
return api('/api/hermes/status')
|
||||
case 'opticz_summary':
|
||||
return api('/api/hermes/summary')
|
||||
case 'opticz_search_clients':
|
||||
return api(`/api/hermes/clients${query(args)}`)
|
||||
case 'opticz_search_products':
|
||||
return api(`/api/hermes/products${query(args)}`)
|
||||
case 'opticz_create_client':
|
||||
return api('/api/hermes/clients', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(args || {}),
|
||||
})
|
||||
case 'opticz_create_repair_sale':
|
||||
return api('/api/hermes/sales/repair', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(args || {}),
|
||||
})
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`)
|
||||
}
|
||||
}
|
||||
|
||||
function send(message) {
|
||||
process.stdout.write(`${JSON.stringify(message)}\n`)
|
||||
}
|
||||
|
||||
async function handle(message) {
|
||||
const { id, method, params } = message
|
||||
|
||||
if (method === 'initialize') {
|
||||
send({
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
result: {
|
||||
protocolVersion: params?.protocolVersion || '2024-11-05',
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
serverInfo: {
|
||||
name: 'opticz-hermes-mcp',
|
||||
version: '0.1.0',
|
||||
},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (method === 'notifications/initialized') return
|
||||
|
||||
if (method === 'tools/list') {
|
||||
send({
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
result: { tools },
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (method === 'tools/call') {
|
||||
try {
|
||||
const result = await callTool(params?.name, params?.arguments || {})
|
||||
send({
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
result: {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
send({
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
result: {
|
||||
isError: true,
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
send({
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
error: {
|
||||
code: -32601,
|
||||
message: `Unsupported method: ${method}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
let buffer = ''
|
||||
process.stdin.setEncoding('utf8')
|
||||
process.stdin.on('data', (chunk) => {
|
||||
buffer += chunk
|
||||
const lines = buffer.split(/\r?\n/)
|
||||
buffer = lines.pop() || ''
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue
|
||||
try {
|
||||
handle(JSON.parse(line))
|
||||
} catch (error) {
|
||||
send({
|
||||
jsonrpc: '2.0',
|
||||
id: null,
|
||||
error: {
|
||||
code: -32700,
|
||||
message: error instanceof Error ? error.message : 'Parse error',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -10,7 +10,8 @@
|
||||
"db:push": "prisma db push",
|
||||
"db:generate": "prisma generate",
|
||||
"db:migrate": "prisma migrate dev",
|
||||
"db:reset": "prisma migrate reset"
|
||||
"db:reset": "prisma migrate reset",
|
||||
"hermes:mcp": "node mini-services/hermes-mcp/server.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
|
||||
76
src/app/api/hermes/clients/route.ts
Normal file
76
src/app/api/hermes/clients/route.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
import { requireHermesAccess } from '@/lib/hermes-auth'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const authError = requireHermesAccess(request)
|
||||
if (authError) return authError
|
||||
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const q = (searchParams.get('q') || '').trim()
|
||||
const limit = Math.min(Number(searchParams.get('limit') || 20), 50)
|
||||
|
||||
const clients = await db.client.findMany({
|
||||
where: q
|
||||
? {
|
||||
OR: [
|
||||
{ nom: { contains: q } },
|
||||
{ prenom: { contains: q } },
|
||||
{ telephone: { contains: q } },
|
||||
{ email: { contains: q } },
|
||||
],
|
||||
}
|
||||
: undefined,
|
||||
orderBy: [{ nom: 'asc' }, { prenom: 'asc' }],
|
||||
take: limit,
|
||||
select: {
|
||||
id: true,
|
||||
nom: true,
|
||||
prenom: true,
|
||||
email: true,
|
||||
telephone: true,
|
||||
ville: true,
|
||||
codePostal: true,
|
||||
createdAt: true,
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ clients })
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const authError = requireHermesAccess(request)
|
||||
if (authError) return authError
|
||||
|
||||
const body = await request.json()
|
||||
const nom = String(body.nom || '').trim()
|
||||
const prenom = String(body.prenom || '').trim()
|
||||
const telephone = String(body.telephone || '').trim()
|
||||
|
||||
if (!nom || !prenom || !telephone) {
|
||||
return NextResponse.json(
|
||||
{ error: 'nom, prenom and telephone are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const existingClient = await db.client.findUnique({ where: { telephone } })
|
||||
if (existingClient) {
|
||||
return NextResponse.json({ client: existingClient, existed: true })
|
||||
}
|
||||
|
||||
const client = await db.client.create({
|
||||
data: {
|
||||
nom,
|
||||
prenom,
|
||||
telephone,
|
||||
email: body.email || null,
|
||||
adresse: body.adresse || null,
|
||||
ville: body.ville || null,
|
||||
codePostal: body.codePostal || null,
|
||||
notes: body.notes || null,
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ client, existed: false }, { status: 201 })
|
||||
}
|
||||
48
src/app/api/hermes/products/route.ts
Normal file
48
src/app/api/hermes/products/route.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
import { requireHermesAccess } from '@/lib/hermes-auth'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const authError = requireHermesAccess(request)
|
||||
if (authError) return authError
|
||||
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const q = (searchParams.get('q') || '').trim()
|
||||
const category = (searchParams.get('category') || '').trim()
|
||||
const inStockOnly = searchParams.get('inStockOnly') !== 'false'
|
||||
const limit = Math.min(Number(searchParams.get('limit') || 25), 75)
|
||||
|
||||
const products = await db.produit.findMany({
|
||||
where: {
|
||||
actif: true,
|
||||
...(inStockOnly ? { stock: { gt: 0 } } : {}),
|
||||
...(category ? { categorie: category } : {}),
|
||||
...(q
|
||||
? {
|
||||
OR: [
|
||||
{ reference: { contains: q } },
|
||||
{ designation: { contains: q } },
|
||||
{ marque: { contains: q } },
|
||||
{ categorie: { contains: q } },
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
orderBy: [{ categorie: 'asc' }, { designation: 'asc' }],
|
||||
take: limit,
|
||||
select: {
|
||||
id: true,
|
||||
reference: true,
|
||||
designation: true,
|
||||
categorie: true,
|
||||
marque: true,
|
||||
prixVenteTTC: true,
|
||||
tva: true,
|
||||
stock: true,
|
||||
stockMin: true,
|
||||
emplacement: true,
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ products })
|
||||
}
|
||||
125
src/app/api/hermes/sales/repair/route.ts
Normal file
125
src/app/api/hermes/sales/repair/route.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { ModePaiement } from '@prisma/client'
|
||||
import { db } from '@/lib/db'
|
||||
import { requireHermesAccess } from '@/lib/hermes-auth'
|
||||
|
||||
const SERVICE_TVA = 20
|
||||
|
||||
async function generateSaleNumber(): Promise<string> {
|
||||
const today = new Date()
|
||||
const year = today.getFullYear()
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0')
|
||||
const salesThisMonth = await db.vente.count({
|
||||
where: {
|
||||
date: {
|
||||
gte: new Date(year, today.getMonth(), 1),
|
||||
lt: new Date(year, today.getMonth() + 1, 1),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return `H${year}${month}${String(salesThisMonth + 1).padStart(4, '0')}`
|
||||
}
|
||||
|
||||
function toHt(ttc: number) {
|
||||
return ttc / (1 + SERVICE_TVA / 100)
|
||||
}
|
||||
|
||||
async function getRepairProduct() {
|
||||
return db.produit.upsert({
|
||||
where: { reference: 'HERMES_SERVICE_REPAIR' },
|
||||
update: {
|
||||
designation: 'Hermes service reparation',
|
||||
actif: true,
|
||||
stock: { increment: 1 },
|
||||
},
|
||||
create: {
|
||||
reference: 'HERMES_SERVICE_REPAIR',
|
||||
designation: 'Hermes service reparation',
|
||||
categorie: 'SERVICE',
|
||||
prixAchatHT: 0,
|
||||
prixVenteTTC: 0,
|
||||
tva: SERVICE_TVA,
|
||||
stock: 999999,
|
||||
stockMin: 0,
|
||||
actif: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const authError = requireHermesAccess(request)
|
||||
if (authError) return authError
|
||||
|
||||
const body = await request.json()
|
||||
const clientId = String(body.clientId || '').trim()
|
||||
const repairType = String(body.repairType || '').trim()
|
||||
const repairDescription = String(body.repairDescription || '').trim()
|
||||
const repairPrice = Math.max(0, Number(body.repairPrice || 0))
|
||||
const additionalCharges = Math.max(0, Number(body.additionalCharges || 0))
|
||||
const paymentMode = (body.paymentMode || 'ESPECES') as ModePaiement
|
||||
|
||||
if (!clientId || !repairType || !repairDescription) {
|
||||
return NextResponse.json(
|
||||
{ error: 'clientId, repairType and repairDescription are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const client = await db.client.findUnique({ where: { id: clientId } })
|
||||
if (!client) {
|
||||
return NextResponse.json({ error: 'Client not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const repairProduct = await getRepairProduct()
|
||||
const totalTTC = repairPrice + additionalCharges
|
||||
const totalHT = toHt(totalTTC)
|
||||
const numero = await generateSaleNumber()
|
||||
|
||||
const sale = await db.vente.create({
|
||||
data: {
|
||||
numero,
|
||||
clientId,
|
||||
statut: 'PAYEE',
|
||||
montantHT: totalHT,
|
||||
montantTVA: totalTTC - totalHT,
|
||||
montantTTC: totalTTC,
|
||||
notes: [
|
||||
`Hermes repair: ${repairType}`,
|
||||
`Description: ${repairDescription}`,
|
||||
additionalCharges > 0 ? `Additional charges: ${additionalCharges.toFixed(2)}` : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n'),
|
||||
lignes: {
|
||||
create: {
|
||||
produitId: repairProduct.id,
|
||||
quantite: 1,
|
||||
prixUnitaireHT: totalHT,
|
||||
prixUnitaireTTC: totalTTC,
|
||||
remise: 0,
|
||||
montantHT: totalHT,
|
||||
montantTTC: totalTTC,
|
||||
},
|
||||
},
|
||||
paiements: {
|
||||
create: {
|
||||
mode: paymentMode,
|
||||
montant: totalTTC,
|
||||
reference: 'HERMES',
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
client: true,
|
||||
lignes: {
|
||||
include: {
|
||||
produit: true,
|
||||
},
|
||||
},
|
||||
paiements: true,
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ sale }, { status: 201 })
|
||||
}
|
||||
16
src/app/api/hermes/status/route.ts
Normal file
16
src/app/api/hermes/status/route.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
import { requireHermesAccess } from '@/lib/hermes-auth'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const authError = requireHermesAccess(request)
|
||||
if (authError) return authError
|
||||
|
||||
await db.$queryRaw`SELECT 1`
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
service: 'OptiqueStock Hermes API',
|
||||
time: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
66
src/app/api/hermes/summary/route.ts
Normal file
66
src/app/api/hermes/summary/route.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
import { requireHermesAccess } from '@/lib/hermes-auth'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const authError = requireHermesAccess(request)
|
||||
if (authError) return authError
|
||||
|
||||
const [clients, activeProducts, lowStock, pendingWorkshop, sales] = await Promise.all([
|
||||
db.client.count(),
|
||||
db.produit.count({ where: { actif: true } }),
|
||||
db.produit.findMany({
|
||||
where: { actif: true },
|
||||
orderBy: { stock: 'asc' },
|
||||
take: 10,
|
||||
select: {
|
||||
id: true,
|
||||
reference: true,
|
||||
designation: true,
|
||||
categorie: true,
|
||||
stock: true,
|
||||
stockMin: true,
|
||||
prixVenteTTC: true,
|
||||
},
|
||||
}),
|
||||
db.vente.count({
|
||||
where: {
|
||||
statutAtelier: {
|
||||
in: ['EN_ATTENTE', 'EN_COURS', 'TERMINE', 'PRET'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
db.vente.findMany({
|
||||
orderBy: { date: 'desc' },
|
||||
take: 8,
|
||||
include: {
|
||||
client: {
|
||||
select: {
|
||||
id: true,
|
||||
nom: true,
|
||||
prenom: true,
|
||||
telephone: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
return NextResponse.json({
|
||||
counts: {
|
||||
clients,
|
||||
activeProducts,
|
||||
pendingWorkshop,
|
||||
},
|
||||
lowStock: lowStock.filter((item) => item.stock <= item.stockMin),
|
||||
recentSales: sales.map((sale) => ({
|
||||
id: sale.id,
|
||||
numero: sale.numero,
|
||||
date: sale.date,
|
||||
statut: sale.statut,
|
||||
statutAtelier: sale.statutAtelier,
|
||||
montantTTC: sale.montantTTC,
|
||||
client: sale.client,
|
||||
})),
|
||||
})
|
||||
}
|
||||
27
src/lib/hermes-auth.ts
Normal file
27
src/lib/hermes-auth.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const DEV_HERMES_KEY = 'hermes-demo-key-change-me'
|
||||
|
||||
export function getHermesApiKey() {
|
||||
return process.env.HERMES_API_KEY || (process.env.NODE_ENV !== 'production' ? DEV_HERMES_KEY : '')
|
||||
}
|
||||
|
||||
export function requireHermesAccess(request: NextRequest) {
|
||||
const expectedKey = getHermesApiKey()
|
||||
const authHeader = request.headers.get('authorization') || ''
|
||||
const bearerKey = authHeader.startsWith('Bearer ') ? authHeader.slice(7).trim() : ''
|
||||
const headerKey = request.headers.get('x-hermes-key') || ''
|
||||
const providedKey = bearerKey || headerKey
|
||||
|
||||
if (!expectedKey || providedKey !== expectedKey) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Hermes access denied',
|
||||
hint: 'Send Authorization: Bearer <HERMES_API_KEY> or x-hermes-key.',
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -7,6 +7,7 @@ export async function proxy(request: NextRequest) {
|
||||
if (
|
||||
pathname.startsWith('/login') ||
|
||||
pathname.startsWith('/api/auth') ||
|
||||
pathname.startsWith('/api/hermes') ||
|
||||
pathname.startsWith('/_next/static') ||
|
||||
pathname.startsWith('/_next/image') ||
|
||||
pathname === '/favicon.ico'
|
||||
|
||||
Reference in New Issue
Block a user