From ba2971c6f317c0acfc8163d640dba7843cb25a98 Mon Sep 17 00:00:00 2001 From: mijinummi Date: Mon, 29 Jun 2026 07:35:29 +0100 Subject: [PATCH 1/3] build(membership_token): implement valid contract entry point and cdylib targets (#1123) --- contracts/membership_token/Cargo.toml | 6 +++++- contracts/membership_token/src/bin/membership-token.rs | 9 +++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/contracts/membership_token/Cargo.toml b/contracts/membership_token/Cargo.toml index 1c1dc22..65b657a 100644 --- a/contracts/membership_token/Cargo.toml +++ b/contracts/membership_token/Cargo.toml @@ -18,4 +18,8 @@ testutils = ["soroban-sdk/testutils"] [[bin]] name = "membership-token" -path = "src/bin/membership-token.rs" \ No newline at end of file +path = "src/bin/membership-token.rs" + +[profile.release] +opt-level = "z" +overflow-checks = true diff --git a/contracts/membership_token/src/bin/membership-token.rs b/contracts/membership_token/src/bin/membership-token.rs index 42424c6..3940e3a 100644 --- a/contracts/membership_token/src/bin/membership-token.rs +++ b/contracts/membership_token/src/bin/membership-token.rs @@ -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); \ No newline at end of file From 0f9bd5a1e04c944db5407531835fd2d1360e226f Mon Sep 17 00:00:00 2001 From: mijinummi Date: Mon, 29 Jun 2026 07:40:01 +0100 Subject: [PATCH 2/3] feat(admin): build access control management page with 30s logging pool (#1119) --- .../app/admin/access-control/devices-tab.tsx | 161 ++++++++++++++++++ .../app/admin/access-control/logs-tab.tsx | 104 +++++++++++ frontend/app/admin/access-control/page.tsx | 51 ++++++ 3 files changed, 316 insertions(+) create mode 100644 frontend/app/admin/access-control/devices-tab.tsx create mode 100644 frontend/app/admin/access-control/logs-tab.tsx create mode 100644 frontend/app/admin/access-control/page.tsx diff --git a/frontend/app/admin/access-control/devices-tab.tsx b/frontend/app/admin/access-control/devices-tab.tsx new file mode 100644 index 0000000..b9ae092 --- /dev/null +++ b/frontend/app/admin/access-control/devices-tab.tsx @@ -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([]); + 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 ( +
+
+

Hardware Peripherals Directory

+ +
+ +
+ + + + + + + + + + + + {devices.map((device) => ( + + + + + + + + ))} + +
Device NameType IdentifierLocation ZoneTelemetry StatusLast Active Heartbeat
{device.name} + + {device.type.replace('_', ' ')} + + {device.location} + + + {device.status} + + {new Date(device.lastSeen).toLocaleString()}
+
+ + {/* Registration Modal Overlay Component */} + {isModalOpen && ( +
+
+

Register New Reader Node

+
+
+ + 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" + /> +
+
+ + +
+
+ + 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" + /> +
+
+ + +
+
+
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/frontend/app/admin/access-control/logs-tab.tsx b/frontend/app/admin/access-control/logs-tab.tsx new file mode 100644 index 0000000..33fbe2d --- /dev/null +++ b/frontend/app/admin/access-control/logs-tab.tsx @@ -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([]); + const [filterAction, setFilterAction] = useState('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 ( +
+
+

Perimeter Access Log Ledger

+ + {/* Dynamic Filters Bar */} +
+ + +
+
+ +
+ + + + + + + + + + + + {logs.map((log) => ( + + + + + + + + ))} + +
Timestamp EventMember NameTarget Reader TerminalMethod VectorVerification Action
{new Date(log.timestamp).toLocaleString()}{log.memberName || Unknown / Unregistered}{log.deviceName} + + {log.method} + + +
+ + {log.action} + + {log.action === 'DENIED' && log.denyReason && ( + + Reason: {log.denyReason} + + )} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/app/admin/access-control/page.tsx b/frontend/app/admin/access-control/page.tsx new file mode 100644 index 0000000..352e17f --- /dev/null +++ b/frontend/app/admin/access-control/page.tsx @@ -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 ( +
+
+
+

Access Control System

+

+ Configure hub hardware readers, monitor entry attempts, and oversee perimeter security parameters. +

+
+ + {/* Tab Selector Links */} +
+ + +
+
+ + {/* Primary Container Render Outlets */} +
+ {activeTab === 'devices' ? : } +
+
+ ); +} \ No newline at end of file From 89191817b0f2d3dbf72af7786a5209fc71e76150 Mon Sep 17 00:00:00 2001 From: mijinummi Date: Mon, 29 Jun 2026 07:51:48 +0100 Subject: [PATCH 3/3] feat(public-booking): implement unauthenticated day-pass wizard with paystack flows (#1118) --- frontend/app/book/page.tsx | 219 +++++++++++++++++++++++++++++ frontend/app/book/success/page.tsx | 34 +++++ 2 files changed, 253 insertions(+) create mode 100644 frontend/app/book/page.tsx create mode 100644 frontend/app/book/success/page.tsx diff --git a/frontend/app/book/page.tsx b/frontend/app/book/page.tsx new file mode 100644 index 0000000..010afe1 --- /dev/null +++ b/frontend/app/book/page.tsx @@ -0,0 +1,219 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; + +interface Workspace { + id: string; + name: string; + description: string; + pricePerDay: number; + imageUrl?: string; +} + +export default function PublicDayPassBookingWizard() { + const [step, setStep] = useState(1); + const [workspaces, setWorkspaces] = useState([]); + const [loading, setLoading] = useState(false); + + // Form State Values + const [selectedWorkspace, setSelectedWorkspace] = useState(null); + const [bookingDate, setBookingDate] = useState(''); + const [userDetails, setUserDetails] = useState({ fullName: '', email: '', phone: '' }); + + useEffect(() => { + // Fetch day-pass eligible spaces exposed by the public endpoint matrix + fetch('/api/v1/bookings/public/eligible-workspaces') + .then((res) => res.json()) + .then((json) => setWorkspaces(json.data || [])) + .catch((err) => console.error('Error listing public workspaces:', err)); + }, []); + + const handlePayNow = async () => { + if (!selectedWorkspace || !bookingDate) return; + setLoading(true); + + try { + const res = await fetch('/api/v1/bookings/public/day-pass', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + workspaceId: selectedWorkspace.id, + date: bookingDate, + ...userDetails, + }), + }); + + const json = await res.json(); + if (json.authorizationUrl) { + // Hand off checkout orchestration directly to Paystack payment gateway routing paths + window.location.href = json.authorizationUrl; + } else { + alert('Failed to initialize payment gateway checkout channel.'); + setLoading(false); + } + } catch (err) { + console.error('Error dispatching public day-pass order request:', err); + setLoading(false); + } + }; + + return ( +
+
+ + {/* Step Indicator Header Navigation Map */} +
+

Book a Day Pass

+
+ 1. Space + + 2. Details + + 3. Checkout +
+
+ + {/* STEP 1: WORKSPACE SELECTION CARD SECTORS */} + {step === 1 && ( +
+

Choose Your Hot Desk or Space & Target Execution Date

+
+ + setBookingDate(e.target.value)} + className="max-w-xs border border-slate-300 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-blue-500" + /> +
+ +
+ {workspaces.map((space) => ( +
setSelectedWorkspace(space)} + className={`cursor-pointer border p-5 rounded-xl transition-all flex flex-col justify-between ${ + selectedWorkspace?.id === space.id + ? 'border-blue-600 bg-blue-50/50 shadow-md' + : 'border-slate-200 hover:border-slate-300 hover:shadow-sm' + }`} + > +
+

{space.name}

+

{space.description}

+
+
+ Daily Pass rate + {space.pricePerDay} NGN +
+
+ ))} +
+ +
+ +
+
+ )} + + {/* STEP 2: USER METADATA IDENTIFICATION INPUT CHANNELS */} + {step === 2 && ( +
+

Enter Your Contact Information

+
+
+ + setUserDetails({ ...userDetails, fullName: e.target.value })} + /> +
+
+ + setUserDetails({ ...userDetails, email: e.target.value })} + /> +
+
+ + setUserDetails({ ...userDetails, phone: e.target.value })} + /> +
+
+ +
+ + +
+
+ )} + + {/* STEP 3: SUMMARY VERIFICATION & PAYSTACK CHECKOUT REDIRECTS */} + {step === 3 && ( +
+

Review Your Day Pass Booking Summary

+
+
Selected Space:{selectedWorkspace?.name}
+
Scheduled Visit Date:{bookingDate}
+
Pass Holder:{userDetails.fullName}
+
Contact Email:{userDetails.email}
+
+ Total Due Amount: + {selectedWorkspace?.pricePerDay} NGN +
+
+ +
+ + +
+
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/app/book/success/page.tsx b/frontend/app/book/success/page.tsx new file mode 100644 index 0000000..73d1ee2 --- /dev/null +++ b/frontend/app/book/success/page.tsx @@ -0,0 +1,34 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; + +export default function PublicBookingSuccessScreen() { + return ( +
+
+
+ + + +
+ +
+

Booking Confirmed!

+

+ Thank you for booking a pass. Your access credential barcode and digital pass receipt have been sent straight to your email address. +

+
+ +
+ + Book Another Day Pass + +
+
+
+ ); +} \ No newline at end of file