Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion contracts/membership_token/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,8 @@ testutils = ["soroban-sdk/testutils"]

[[bin]]
name = "membership-token"
path = "src/bin/membership-token.rs"
path = "src/bin/membership-token.rs"

[profile.release]
opt-level = "z"
overflow-checks = true
9 changes: 5 additions & 4 deletions contracts/membership_token/src/bin/membership-token.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
fn main() {
// This is a placeholder binary file for contract deployment
println!("Membership Token Contract Binary");
}
#![no_std]
#![no_main]

// Entry point mapping macro expanding contract capabilities for the Stellar/Soroban network
soroban_sdk::contractimpl!(membership_token::MembershipTokenContract);
161 changes: 161 additions & 0 deletions frontend/app/admin/access-control/devices-tab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
'use client';

import React, { useEffect, useState } from 'react';

interface Device {
id: string;
name: string;
type: 'QR_READER' | 'RFID' | 'SMART_LOCK';
location: string;
status: 'ONLINE' | 'OFFLINE';
lastSeen: string;
hardwareId: string;
}

export default function DevicesTab() {
const [devices, setDevices] = useState<Device[]>([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [formData, setFormData] = useState({ name: '', type: 'QR_READER', location: 'Main Entrance', hardwareId: '' });

const fetchDevices = async () => {
try {
const res = await fetch('/api/v1/access-control/devices');
const json = await res.json();
setDevices(json.data || []);
} catch (err) {
console.error('Failed to resolve hardware registration collections:', err);
}
};

useEffect(() => {
fetchDevices();
}, []);

const handleRegisterSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
await fetch('/api/v1/access-control/devices', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
setIsModalOpen(false);
setFormData({ name: '', type: 'QR_READER', location: 'Main Entrance', hardwareId: '' });
fetchDevices();
} catch (err) {
console.error('Error recording hardware registration profile:', err);
}
};

return (
<div className="p-6 space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold">Hardware Peripherals Directory</h2>
<button
onClick={() => setIsModalOpen(true)}
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm font-medium shadow transition-colors"
>
Register Access Device
</button>
</div>

<div className="overflow-x-auto border border-slate-100 rounded-lg">
<table className="w-full text-left border-collapse text-sm">
<thead>
<tr className="bg-slate-50 border-b border-slate-200 text-slate-600 font-medium">
<th className="p-4">Device Name</th>
<th className="p-4">Type Identifier</th>
<th className="p-4">Location Zone</th>
<th className="p-4">Telemetry Status</th>
<th className="p-4">Last Active Heartbeat</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{devices.map((device) => (
<tr key={device.id} className="hover:bg-slate-50/50 transition-colors">
<td className="p-4 font-medium text-slate-900">{device.name}</td>
<td className="p-4">
<span className="px-2.5 py-1 text-xs font-semibold rounded-full bg-slate-100 text-slate-800 border border-slate-200">
{device.type.replace('_', ' ')}
</span>
</td>
<td className="p-4 text-slate-600">{device.location}</td>
<td className="p-4">
<span className={`inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-semibold border ${
device.status === 'ONLINE'
? 'bg-emerald-50 text-emerald-700 border-emerald-200'
: 'bg-rose-50 text-rose-700 border-rose-200'
}`}>
<span className={`w-1.5 h-1.5 rounded-full ${device.status === 'ONLINE' ? 'bg-emerald-500' : 'bg-rose-500'}`} />
{device.status}
</span>
</td>
<td className="p-4 text-slate-500">{new Date(device.lastSeen).toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
</div>

{/* Registration Modal Overlay Component */}
{isModalOpen && (
<div className="fixed inset-0 bg-slate-900/40 backdrop-blur-sm flex justify-center items-center p-4 z-50 animate-fade-in">
<div className="bg-white rounded-xl shadow-xl max-w-md w-full border border-slate-100 overflow-hidden p-6 space-y-4">
<h3 className="text-lg font-bold text-slate-900">Register New Reader Node</h3>
<form onSubmit={handleRegisterSubmit} className="space-y-4">
<div>
<label className="block text-xs font-semibold text-slate-600 uppercase mb-1">Device Name</label>
<input
type="text"
required
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="w-full border border-slate-300 rounded-lg p-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="e.g., South Turnstile Core"
/>
</div>
<div>
<label className="block text-xs font-semibold text-slate-600 uppercase mb-1">Reader Mechanism Type</label>
<select
value={formData.type}
onChange={(e) => setFormData({ ...formData, type: e.target.value })}
className="w-full border border-slate-300 rounded-lg p-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="QR_READER">QR Code Canvas Reader</option>
<option value="RFID">RFID Proximity Scanner</option>
<option value="SMART_LOCK">Smart Lock Bolt Actuator</option>
</select>
</div>
<div>
<label className="block text-xs font-semibold text-slate-600 uppercase mb-1">Hardware MAC/ID Footprint</label>
<input
type="text"
required
value={formData.hardwareId}
onChange={(e) => setFormData({ ...formData, hardwareId: e.target.value })}
className="w-full border border-slate-300 rounded-lg p-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="e.g., HW-FLX-00921"
/>
</div>
<div className="flex justify-end gap-3 pt-2">
<button
type="button"
onClick={() => setIsModalOpen(false)}
className="px-4 py-2 text-sm font-medium text-slate-600 hover:bg-slate-50 rounded-lg transition-colors"
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg shadow transition-colors"
>
Provision Device
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}
104 changes: 104 additions & 0 deletions frontend/app/admin/access-control/logs-tab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
'use client';

import React, { useEffect, useState } from 'react';

interface AccessLog {
id: string;
timestamp: string;
memberName: string | null;
deviceName: string;
method: 'QR' | 'RFID' | 'MANUAL';
action: 'GRANTED' | 'DENIED';
denyReason?: string;
}

export default function LogsTab() {
const [logs, setLogs] = useState<AccessLog[]>([]);
const [filterAction, setFilterAction] = useState<string>('ALL');

const fetchLogs = async () => {
try {
const url = filterAction === 'ALL'
? '/api/v1/access-control/logs'
: `/api/v1/access-control/logs?action=${filterAction}`;
const res = await fetch(url);
const json = await res.json();
setLogs(json.data || []);
} catch (err) {
console.error('Error reading real-time perimeter logs stream:', err);
}
};

// 30-Second Real-Time Heartbeat Polling Loop
useEffect(() => {
fetchLogs();
const pollingInterval = setInterval(fetchLogs, 30000);
return () => clearInterval(pollingInterval);
}, [filterAction]);

return (
<div className="p-6 space-y-4">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 bg-slate-50 p-4 rounded-xl border border-slate-200">
<h2 className="text-xl font-semibold text-slate-800">Perimeter Access Log Ledger</h2>

{/* Dynamic Filters Bar */}
<div className="flex items-center gap-2">
<label className="text-xs font-bold text-slate-500 uppercase tracking-wider">Filter Action:</label>
<select
value={filterAction}
onChange={(e) => setFilterAction(e.target.value)}
className="border border-slate-300 rounded-lg p-2 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="ALL">All Incidents</option>
<option value="GRANTED">Access Granted</option>
<option value="DENIED">Access Denied</option>
</select>
</div>
</div>

<div className="overflow-x-auto border border-slate-100 rounded-lg">
<table className="w-full text-left border-collapse text-sm">
<thead>
<tr className="bg-slate-50 border-b border-slate-200 text-slate-600 font-medium">
<th className="p-4">Timestamp Event</th>
<th className="p-4">Member Name</th>
<th className="p-4">Target Reader Terminal</th>
<th className="p-4">Method Vector</th>
<th className="p-4">Verification Action</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{logs.map((log) => (
<tr key={log.id} className="hover:bg-slate-50/50 transition-colors">
<td className="p-4 text-slate-500 font-mono text-xs">{new Date(log.timestamp).toLocaleString()}</td>
<td className="p-4 font-medium text-slate-900">{log.memberName || <span className="text-slate-400 italic">Unknown / Unregistered</span>}</td>
<td className="p-4 text-slate-600">{log.deviceName}</td>
<td className="p-4">
<span className="px-2 py-0.5 rounded text-xs font-medium bg-slate-100 text-slate-700">
{log.method}
</span>
</td>
<td className="p-4">
<div className="flex flex-col gap-1">
<span className={`inline-flex max-w-fit px-2.5 py-0.5 rounded-full text-xs font-semibold ${
log.action === 'GRANTED'
? 'bg-emerald-100 text-emerald-800'
: 'bg-rose-100 text-rose-800'
}`}>
{log.action}
</span>
{log.action === 'DENIED' && log.denyReason && (
<span className="text-xs text-rose-500 max-w-xs truncate" title={log.denyReason}>
Reason: {log.denyReason}
</span>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
51 changes: 51 additions & 0 deletions frontend/app/admin/access-control/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'use strict';

import React, { useState } from 'react';
import DevicesTab from './devices-tab';
import LogsTab from './logs-tab';

export default function AccessControlDashboard() {
const [activeTab, setActiveTab] = useState<'devices' | 'logs'>('devices');

return (
<div className="p-6 space-y-6 max-w-7xl mx-auto text-slate-900">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center border-b border-slate-200 pb-4 gap-4">
<div>
<h1 className="text-3xl font-bold tracking-tight">Access Control System</h1>
<p className="text-sm text-slate-500">
Configure hub hardware readers, monitor entry attempts, and oversee perimeter security parameters.
</p>
</div>

{/* Tab Selector Links */}
<div className="flex bg-slate-100 p-1 rounded-lg border border-slate-200">
<button
onClick={() => setActiveTab('devices')}
className={`px-4 py-2 text-sm font-medium rounded-md transition-all ${
activeTab === 'devices'
? 'bg-white text-blue-600 shadow-sm'
: 'text-slate-600 hover:text-slate-900'
}`}
>
Registered Devices
</button>
<button
onClick={() => setActiveTab('logs')}
className={`px-4 py-2 text-sm font-medium rounded-md transition-all ${
activeTab === 'logs'
? 'bg-white text-blue-600 shadow-sm'
: 'text-slate-600 hover:text-slate-900'
}`}
>
Real-Time Access Logs
</button>
</div>
</div>

{/* Primary Container Render Outlets */}
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
{activeTab === 'devices' ? <DevicesTab /> : <LogsTab />}
</div>
</div>
);
}
Loading
Loading